Шаблоны — контекст, рендеринг и сокращения

Шаблон – специально подготовленный образец, на основе которого генерируется веб-страница. За работы с шаблонами в Django отвечает система, которую принято называть шаблонизатором.

Такой шаблон представляет собой классический HTML-код, в который добавлены специальные инструкторы шаблонизатора: фильтры, директивы и теги.  Фильтры предназначены для выполнения определенных преобразований данных перед выводом, директивы позволяют вывести какое либо значение, а теги позволяют управлять генерированием содержимого.

По умолчанию шаблонизатор осуществляет поиск шаблонов только в каталогах templates, которые находятся в папках приложений, файлы должны иметь расширение html. Метод поиска можно изменить через настройки.

Приступим к созданию первого шаблона. Для этого в каталоге bboard создадим папку templates, а в ней – вложенный каталог bboar. Перейдя в новую папку, создадим собственный шаблон – файл index.html:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Главная :: Доска объявлений</title>
    </head>
    <body>
        <h1>Объявления</h1>
        {% for bb in bbs %}
        <div>
            <h2>{{ bb.title }}</h2>
            <p>{{ bb.content }}</p>
            <p>{{ bb.published | date: "d.m.Y H:i:s" }}</p>
        </div>
        {% endfor %}
    </body>
</html>

В целом это обычный код HTML, однако присутствуют команды шаблонизатора.

Рассмотрим данный тег:

{% for bb in bbs %}
. . . 
{% endfor %}

Данная конструкция работает аналогично циклу for языка Python – осуществляется перебор последовательности, которая хранится в переменной bbs (она входит в состав контекста текущего шаблона, будет рассмотрена позднее). Очередной элемент присваивается переменной bb, которая используется в теле цикла. В нашем случае переменная bbs хранит набор объявлений, а переменная bb – текущее объявление.

Далее рассмотрим директиву шаблонизатора:

{{ bb.title }}

В ней дается команда извлечь атрибут title объекта, который хранится в переменной bb, и вывести его значение в соответствующее место в коде.

Также используется фильтр date:

<p>{{ bb.published | date: "d.m.Y H:i:s" }}</p>

Он позволяет преобразовать значение атрибута published (временная метка) в определенный формат, который указывается в виде специальной строки после двоеточия. В нашем примере строка задает следующий формат вывода даты: <число>.<месяц цифрой>.<год – четыре цифры> <часы 24-ой формат>:<минуты>:<секунды>.

Контекст, рендеринг и сокращения

Вернемся к модулю views.py приложения bboard и исправим текущий код на следующий:

from django.http import HttpResponse
from django.template import loader

from .models import Bb 
def index(request):
    template = loader.get_template('bboard/index.html')
    bbs = Bb.objects.order_by('-published')
    context = {'bbs': bbs}
    return HttpResponse(template.render(context, request))

В первую очередь загружается шаблон при помощи функции get_template() (входит в состав модуля django.template.loader). Путь к файлу указывается в параметре в виде строки. Данная функция возвращает экземпляр класса Template, который представляет собой шаблон из указанного файла.

Далее формируется контекст шаблона – данные, которые необходимо вывести на сгенерированной HTML-странице. Контекст шаблона должен быть сформирован как обычный словарь Python, элементы которого преобразуются в переменные для использования внутри шаблона. Так, элемент bbs созданного контекста шаблона и содержащий все объявления, преобразуется в переменную bbs, которая и используется для вывода в шаблоне.

Завершающий этап – выполнение рендеринга шаблона (генерация готовой веб-страницы). Для этого вызывается метод render(), который относится к классу Template. Методу передается сформированный ранее контекст шаблона, а также экземпляр класса HttpRequest, который представляет собой клиентский запрос (передается через параметр request). Результат представляет собой готовый к выводу HTML-код, передается конструктору класса HttpResponse для подготовки ответа.

После сохранения обоих файлов можно снова запустить отладочный сервер и оценить полученный результат:

Первая версия контроллера index() использует для рендеринга низкоуровневые инструменты, что заметно усложняет код в целом. Однако Django предоставляет и инструменты более высокого уровня – готовые функции-сокращения (shortcuts). К примеру, функция-сокращение render(), которая входит в состав модуля django.shortcuts, обеспечивает выполнение и загрузки и рендеринга шаблона. Для демонстрации возможности исправим код модуля views.py:

from django.shortcuts import render
from .models import Bb
def index(request):
    bbs = Bb.objects.order_by('-published')
    return render(request, 'bboard/index.html', {'bbs': bbs})

После сохранения файла и обновления страницы видно, что результат выполнения кода аналогичный, однако сам код более компактен.

Консоль Django, работа с моделями

Фреймворк имеет в составе собственную консоль Django (редакция консоли Python Shell). Основное отличие – в пути поиска модулей автоматически добавляется каталог проекта, из которого данная консоль была запущена.

Для запуска консоли необходимо выполнить команду:

manage.py shell

В случае успешного запуска отобразится приглашение >>>, которое предлагает ввести команду на языке Python и получить результат.

Использует данный инструмент для создания первого объявления (модель Bb).

>>> from bboard.models import Bb
>>> b1 = Bb(title='Продается дача',  content='Общество "Маяк". ' + \
                'Два этажа, стены кирпич, газ, свет.', price=600000)

Запись модели осуществляется аналогично работе любого другого класса – через вызов конструктора. Значение нужных полей можно передать через именнованные параметры.

При создании новой записи данным методом сохранение в базу данных не происходит – данные хранятся в ОЗУ. Сохранение осуществляется при вызове метода save() без указания параметров.

>>> b1.save()

Для проверки можно попытаться получить значение ключевого поля:

>>> b1.pk
1

Атрибут класса pk присутствует у всех моделей и содержит ключевые поля. Получить его значение можно только после добавления записи в базу данных.

Аналогично можно обратиться к любому полю записи, указав соответствующий атрибут:

>>> b1.title
'Продается дача'

Можно сначала создать запись без данных, вызвав конструктор класса без параметров, а все поля добавить позднее:

>>> b2 = Bb()
>>> b2.title = 'Земельный участок'
>>> b2.content = '4 сотки'
>>> b2.save()
>>> b2.pk
2

Все классы моделей также имеют поддержку атрибута класса object, который хранит диспетчер записей. Данный объект является экземпляром класса Manager и содержит все записи текущей модели.

Метод create() диспетчера позволяет быстро создать новую запись, принимая значения полей в виде именованных параметров. При этом сразу происходит сохранение новой записи и возвращение ее в качестве результата.

Выведем заголовки и соответствующие ключи всех объявлений модели Bb:

>>> for x in Bb.objects.all():
        print(x.pk, ': ', x.title)
1: Продается дача
2: Автомобиль

У диспетчера записей присутствует метод all(), который возвращает набор записей – последовательность содержит все записи модели, которые можно обработать при помощи стандартного цикла. Набор записей – экземпляр класса QuerySet, отдельные записи представляются экземплярами класса соответствующей модели.

Отсортируем записи по заголовку:

>>> for x in Bb.objects.order_by ('title'):
         print(x.pk, ': ', x.title) 
1: Автомобиль
2: Продается дача

Метод order_by() сортирует полученный набор записей (поле сортировки указывается в параметре), возвращая отсортированный набор данных.

Метод filter() позволяет отфильтровать записи по определенным критериям. К примеру, чтобы набор содержал только записи, в которых определенное поле (например, заголовок) содержит указанное значение. Вызов метода возвращает другой диспетчер записей, который содержит только записи, удовлетворяющие условию фильтрации.

В качестве примера найдем объявление, которое имеет ключ 3:

>>> i = Bb.objects.get(pk=3)
>>> i.title
'Стиральная машинка'

Для поиска объявлений также можно воспользоваться методом get(), который имеет аналогичный метод вызова, однако возвращает только первую найденную запись.

Для удаления записей служит метод delete(), который возвращает количество удаленных записей.

Для выхода из консоли необходимо воспользоваться командой exit().

Вернемся к контролеру и сделаем так, чтобы он выводил список всех объявлений с сортировкой по дате публикации.

Откроем для редактирования модуль views.py (приложение bboard) и изменим код:

from django.http imort HttpResponse
from .models import Bb
def index(request):
    s = 'Объявления\r\n\r\n'
    for bb in Bb.objects.order_by('-published'):
        s += bb.title + '\r\n' + bb.content + '\r\n\r\n'
    return HttpResponse(s, content_type='text/plain; charset=utf-8')

Чтобы объявления сортировались по убыванию мы перед именем поля published добавили знак '-'. Список объявлений выводится как обычный текст, для разбивки на строки используются служебные символы \r\n.

За отправку ответа клиента отвечает экземпляр класса HttpResponse, в котором при помощи именованного параметра content_type указали тип данных: обычный текст в кодировке UTF-8. Без указания кодировки браузер может некорректно отобразить данные.


Таким образом можно сформировать и полноценную HTML-страницу, отобразив соответствующий код. Однако удобнее использовать специальные шаблоны.

Модели и миграции

Теперь настала пора сделать так, чтобы вместо примитивного текстового сообщения на страницу выводились полноценные сообщения, выгруженные из настоящей базы данных. Первый шаг – объявить модель.

Модель – специальный класс, описывающий конкретную таблицу в подключенной базе данных. В частности, необходимо описать доступный набор полей. При этом отдельный экземпляр класса - это отдельная запись таблицы (например, определенное сообщение блога, объявление и т.п.), что позволяет редактировать запись или выводить ее пользователю. При описании модели нет необходимости использовать привязку к определенной базе данных.

Для объявления модели используется файл models.py соответствующего пакета приложения. Первоначально данный модуль пуст.

Объявим модель Bb (объявления), указав следующие поля:

  • title – заголовок объявления. Поле обязательно к заполнению и имеет строковой тип (до 50 символов);
  • content – текст объявления (тип memo);
  • price – цена товара (вещественное число);
  • published – дата добавления объявления (временная метка, по умолчанию – текущая дата, индексированное поле);

Если запущен отладочный сервер, его необходимо остановить. Далее необходимо открыть файл models.py приложения bboard и добавить следующий код:

From django.db import models
Class Bd(models.Model):
    title = models.CharField(max_length=50)
    content = models.TextField(null=True, blank=True)
    price = models.FloatField(null=True, blank=True)
    published = models.DateTimeField(auto_now_add=True, db_index=True)

Объявленная модель должна представлять собой подкласс класса Model (модуль django.db.models). Конкретные поля модели объявляются как атрибуты класса, которым необходимо присвоить экземпляры классов (поля). Параметры полей необходимо указывать в конструкторах классов соответствующих полей при помощи именованных параметров.

В новой модели использовались следующие классы полей:

  • CharField – стандартное строковое поле, максимальная длина ограничена (параметр max_length);
  • TextField – текстовое поле без ограничения длины (memo-поле). Присвоение параметрам blank и null значения True укажет, что данное поле пользователь может не заполнять (по умолчанию все поля обязательны к заполнению);
  • DateTimeField – поле предназначено для хранения временной метки. Параметр auto_now_add позволяет указать, что при создании записи в данное поле необходимо занести текущее время и дату. Параметр db_index инициирует создание индекса для данного поля (объявления можно будет сортировать по дате).

Обычно таблицы баз банных имеют в составе специальное ключевое поле, необходимое для хранения уникальных значений для однозначной идентификации текущей записи (ключей). Как правило, поле имеет целочисленный тип и является автоинкрементным. В этом случае СУБД автоматически добавляет уникальный идентификатор в момент создания новой записи. При создании моделей в Django поле такого типа можно не объявлять – оно будет создано автоматически.

После сохранения файла необходимо сгенерировать новую миграцию, которая позволит автоматически создать в базе данных все необходимые таблицы.

Стоит помнить, что по умолчанию параметры нового проекта в Django настроены на использование БД SQLite. База данных хранится в виде файла db.sqlite3 (находится в каталоге проекта), который создается автоматически при первом обращении.

Миграции

Миграция – программа, которая генерируется на основе созданных моделей и предназначена для внесения изменений в базу данных: таблицы, правила, индексы, поля, а также связи.

Для запуска миграции, основанной на модели Bd, необходимо переключится на командную строку, убедится, что открыт каталог проекта, а отладочный сервер остановлен. Далее необходимо использовать команду:

manage.py makemigrations bboard

Данная команда начинает генерирование миграций для всех моделей, добавленных пользователем в указанное приложение.

Для хранения миграций используется пакет migrations, который находится в пакете текущего приложения. Файл первой миграции автоматически получает название 0001_initial.py.

В процессе выполнения миграции генерируются команды SQL, которые и обеспечивают создание нужной структуры БД. Посмотреть SQL-код можно при помощи команды в таком формате:

manage.py sqlmigrate bboard 0001

Для применения созданной миграции служит команда:

manage.py migrate

Данная команда выполняет все миграции (всех приложений), которые ранее не выполнялись. Стоит учитывать, что многие стандартные приложения, идущие в составе Django, используют базу данных для хранения своих данных. При этом для создания всех таблиц также используется готовый набор миграций.

Маршруты и маршрутизатор

Для связи определенного интернет-адреса с соответствующим контролером необходимо выполнить следующие шаги:

  • объявить связь определенного пути или шаблона с нужным контроллером.

Путь – часть URL, расположенная между адресом текущего хоста и GET-параметрами. Начальный слеш в шаблонном пути не ставится, в конце должен присутствовать.

  • оформить все объявленные маршруты в специальный список маршрутов;
  • маршруты необходимо оформить в заранее определенном формате, чтобы подсистема маршрутизатора смогла корректно с ним взаимодействовать.

После поступления запроса Django выделяет из переданного URL путь и передает его маршрутизатору. Далее происходит последовательное сравнение его с элементами списка маршрутов (сверху вниз). Первое найденное совпадение инициирует передачу управления контролеру, который связан с данным шаблонным путем.

Чтобы при переходе по адресу http://localhost:8000/bboard/ активировался контроллер index(), необходимо явно связать его с шаблоном URL bboard/.

Для этого необходимо открыть файл urls.py, расположенный в каталоге с именем проекта. По умолчание содержимое файлы следующее:

from django.contrib import admin
from django.urls import path
urlpatterns = [
    path('admin/ ', admin.site.urls),
]

Список маршрутов присваивается переменной urlpatterns в виде обычного списка Python. Каждый элемент данного списка должен представляться как результат, полученный от функции path() (модуль django.urls). Данная функция принимает на вход ссылку на соответствующий контроллер-функцию и строку с шаблоном URL.

В функцию path() также можно передать перечень маршрутов уровня приложения (в качестве второго параметра). Этот вариант и продемонстрирован в листинге 1.

Теперь добавим в список по умолчанию новый маршрут, обрабатывающий путь bboard/, связывая его с контролером-функцией index(). Для этого содержимое файла urls.py необходимо привести к следующему виду:

from django.contrib import admin
from django.urls import path
from bboard.views import index
urlpatterns = [
    path(' bboard/', index),
    path('admin/ ', admin.site.urls),
]

После сохранения файла запустим отладочный веб-сервер и откроем страницу http://localhost:8000/bboard/.

Маршрутизатор Django не требует, чтобы нужный адрес совпадал полностью, достаточно, если совпадет только начало.

Ранее упоминалось, что функция path() позволяет использовать в качестве второго параметра не ссылку на контроллер-функцию, а другой список маршрутов (по принципу вложенных каталогов на ПК). В этом случае маршрутизатор после фиксации совпадения удаляет начальную часть (текущий поиск) и продолжает проверку по вложенному списку.

Это позволяет сделать удобную иерархию списков маршрутов, что облегчает работу с большими проектами. В список пакета конфигурации (уровень проект) запишем маршрут, который указывает на вложенный список конкретного приложения (уровень приложения). А уже во вложенном списке укажем ссылки на нужные контроллеры.

Сначала изменим список маршрутов уровня приложения bboard. Для этого необходимо в каталоге bboard создать файл urls.py и добавить в него следующий код:

from django.urls import path
from bboard.views import index
urlpatterns = [
    path(' ', index),
]

Далее необходимо изменить код файла urls.py пакета конфигурации:

from django.contrib import admin
from django.urls import path, include
urlpatterns = [
    path(' bboard/', include('bboard.urls')),
    path('admin/ ', admin.site.urls),
]

Как видно, вторым параметром функции path(), указывается вложенный список маршрутов, который представляет собой результат, полученный от функции include() (модуль django.urls). Данная функция принимает только один параметр – путь к модулю, где указан список маршрутов нижележащего уровня.

При получении запроса http://localhost:8000/bboard/ маршрутизатор обнаружит совпадение с bboard/ в первом маршруте. Данный префикс будет автоматически удален, после чего будет продолжен поиск с новым путем (в данном случае это пустая строка). Новый путь совпадает с единственной записью вложенного списка, что приведет к запуску функции index() – откроется интернет-страница с соответствующей фразой.

В списке уровня проекта также присутствует второй маршрут (добавляется по умолчанию):

path('admin/', admin.site.urls),

Данный маршрут обеспечивает связь шаблона admin/ с набором маршрутов, который хранится в свойстве urls экземпляра класса AdminSite. Хранится он в переменной site и обеспечивает доступ к административному разделу сайта Django.

Приложения и контролеры

В терминологии Django приложение – отдельный функциональный блок, входящий в состав сайта. Решение о выделении части сайты в отдельное приложение принимает разработчик. Такое приложение может реализовать отдельный раздел интернет-ресурса или подсистему (регистрация пользователя, добавление объявления и т.п.).

Приложение представляется стандартным пакетом Python, который содержит модули с программным кодом. Находится пакет в каталоге проекта – там, где находится пакет конфигурации. При этом имя пакета становится именем нового приложения.

Далее в качестве примера будет приведен алгоритм создания нового приложения bboard , которое обеспечит вывод объявлений, оставленных пользователями.

Если запущен отладочный веб-сервер, его необходимо остановить. Далее нужно убедиться, что вы находитесь в каталоге проекта, после чего выполнить команду:

manage.py startapp bboard

Данная команда утилиты manage.py инициирует создание стандартного шаблона нового (пустого) приложения с именем, указанным в команде.

Рассмотрим, какие файлы создаются при создании приложения. Прежде всего, это новый каталог bboard, расположенный в папке текущего проекта. В нем расположены следующие файлы:

  • migrations – папка для хранения файлов миграции. Изначально в каталоге находится только стандартный файл __init__.py (пустой), указывающий, что это полноценный пакет Python.
  • admin.py – модуль классов-редакторов и основных административных настроек;
  • models.py – модуль для моделей проекта;
  • apps.py – настройки приложения;
  • tests.py – модуль, предназначенный для хранения различных тестов.

После создания приложения его необходимо зарегистрировать в проекте. Для этого нужно в пакете конфигурации найти файл настроек settings.py, открыть его для редактирования (в используемой среде программирования или обычном текстовом редакторе). Далее необходимо найти переменную INSALLED_APPS, в которой хранятся все приложения текущего проекта. По умолчанию данная секция выглядит следующим образом:

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]

Присутствуют только стандартные инструменты, реализующие необходимые подсистемы фреймворка. К примеру, приложение django.contrib.sessions представляет собой подсистему для обслуживания серверных сессий, а приложение django.contrib.auth – обеспечивает разграничение доступа.

В данный список и нужно добавить созданное приложение:

INSTALLED_APPS = [
. . .
'bboard.apps.BboardConfig',
]

Мы добавили строку с путем к классу BboardConfig, в котором описана конфигурация приложения.

После сохранения файла можно сразу попробовать написать первый контролер.

Контролеры

Контролер Django – это код, который активируется при обращении к определенному интернет-адресу (или шаблону URL), выводя в ответ соответствующий контент. Стоит отметить, что в официальной документации обычно используется другой термин – view (представление или вид).

Контроллер Django может быть реализован как функция (обозначают как контроллер-функция) либо как класс (контроллер-класс). Первый вариант более универсален, но их сложнее программировать, во втором случае используется гораздо меньше кода, поэтому такие контролеры удобно использовать для реализации типовых задач (например, вывод определенных товаров).

Изначально для хранения кода контролеров предназначен файл views.py, который автоматически создается в каждом приложении. Однако контролеры можно поместить и в другие модули.

Для примера создадим простой контролер, который будет просто выводить текст на странице. Необходимо открыть файл views.py, удалить код по умолчанию и добавить следующий код:

From django.http import HttpResponse
def index(request):
return HttpResponse("Пример вывода текста на страницу")

Созданный контролер – функция index(), которая выполняет только вывод текста на страницу.

Контроллер-функция имеет обязательный параметр – экземпляр класса HttpRequest, в котором хранится информация о запросе: служебная информация от браузера, запрошенный интернет-адрес, данные, полученные от пользователя и пр. Данный параметр принято обозначать как request, в нашем примере он никак не используется.

В теле функции инициируется создание класса HttpResponse (модуль django.http), который обеспечивает передачу ответа. Содержимое ответа (в нашем случае просто текст) является единственным параметром конструктора класса.

Для просмотра результата необходимо также реализовать связь набранного в браузере интернет адреса с контролером (описано в следующей статье)

Установка фреймворка, первый проект

Инсталлировать Django удобнее при помощи специальной утилиты pip, которая идет в комплекте с Python и предназначена для установки дополнительных библиотек из каталога PyPl. Для этого необходимо запустить командную строку и активировать команду:

pip install django

Стоит учитывать, что если исполняющая среда Python инсталлирована в каталог Program Files (x86) или Program Files, то при установке дополнительных библиотек запускать командную строку необходимо только с правами администратора.

Кроме Django автоматический будут установлены следующие библиотеки:

  • pytz – работа с временными метками;
  • sqlparse – разбор SQL кода;
  • asgiref – реализация интерфейса ASGI, который обеспечивает полноценное взаимодействие эксплуатационного веб-сервера с сайтом.

После завершения инсталляции будет выведено соответствующее сообщение.

Проект Django

Для начала работы необходимо создать новый проект. Проект физически представляет собой папку, в которой хранятся папки и файлы с исходным кодом.

Для создания нового проекта служит команда:

django-admin startproject testsite

Перед использованием команды необходимо перейти в каталог, в котором будут храниться все файлы нового проекта. Утилита django-admin предназначена для выполнения различных команд на уровне администратора. Инструкция startproject позволяет инициировать создание нового проекта с именем, который указывается после команды.

Будет создана следующая структура каталогов:

testsite
    manage.py
   testsite
        __init__.py
        asgi.py
        settings.py
        urls.py
        wsgi

Пака первого уровня – это и есть каталог проекта. Ее имя всегда совпадает с именем проекта, указанным при создании. Внутри содержатся следующие файлы:

  • manage.py – содержит код служебной утилиты, предназначенной для выполнения различных действий над текущим проектом;
  • подкаталог testsites – специальный пакет Python, хранит модули, обеспечивающие конфигурацию пакета в целом;
  • __init__.py – пустой файл, который указывает, что данная папка является полноценным пакетом;
  • settings.py – файл с настройками самого проекта. Содержит параметры работы с базой данных, настройки безопасности, пути к ключевым каталогам и другие;
  • urls.py – описание маршрутов на уровне проекта
  • wsgi – модуль, обеспечивающий связь проекта с веб-сервером (используется интерфейс WSGI);
  • asgi.py (появился в Django 3.0) – модуль, обеспечивающий связь проекта с веб-сервисом при помощи интерфейса ASGI

Отладочный веб-сервер Django

В комплект по умолчанию входит полноценный отладочный веб-сервер (написан на языке Python). Для доступа к утилите необходимо перейти непосредственно в каталог проекта и активировать команду:

manage.py runserver

Если код сайта не содержит ошибок, отобразится сообщение, что ресурс доступен по адресу http://127.0.0.1:8000/ (также может использоваться http://localhost:8000/). По умолчанию всегда используется TCP-порт 8000, но данный параметр можно изменить.

Если запускается пустой проект, по ссылке будет доступна стандартная информационная страница, подтверждающая, что запуск прошел удачно. Для остановки достаточно нажать комбинацию Ctrl + Break.

Введение в графические процессоры

Главным обоснованием добавления SIMD-инструкций к существующим архитектурам послужило то обстоятельство, что многие микропроцессоры в персональных компьютерах и рабочих станциях были подключены к дисплеям, поэтому на графику тратилось все больше вычислительного времени. Поэтому, когда согласно закону Мура количество доступных микропроцессорам транзисторов увеличилось, появился смысл улучшить обработку графики.

Как только закон Мура позволил центральному процессору улучшить обработку графики, он также дал возможность добавить в микросхемы видеографических контроллеров функции ускорения 2D- и ЗD-графики. Более того, в числе самых передовых устройств появились графические карты, как правило, компании Silicon Graphics, которые можно было добавлять к рабочим станциям, что позволяло создавать изображения фотографического качества. Эти самые передовые графические карты были популярны при создании сгенерированных компьютером изображений, что позже было перенесено в телевизионную рекламу, а затем в кинопроизводство. Таким образом, у наращивания рабочих ресурсов видеографических контроллеров была вполне конкретная цель, что во многом было похоже на то, как суперкомпьютеры проложили дорогу микропроцессорам.

Большим стимулом для улучшения обработки графики стала индустрия компьютерных игр, развивавшаяся как на базе персональных компьютеров, так и на специальных игровых консолях, таких как Sony PlayStation. Быстро разраставшийся рынок компьютерных игр подтолкнул многие компании на инвестирование все более крупных средств в развитие быстродействующего графического оборудования, таким образом, темпы развития графической обработки стали более высокими по сравнению с темпами развития вычислений общего назначения в серийно выпускаемых микропроцессорах.

Сообщество разработчиков графических устройств и компьютерных игр имело несколько иные цели, чем сообщество разработчиков микропроцессоров, и выработало свой собственный стиль обработки данных и свою собственную терминологию. Как только графические процессоры стали мощнее, их начали называть графическими процессорными устройствами (Graphics Processing Unit, GPlf), чтобы отличать их от центральных процессорных устройств, CPU. Рассмотрим некоторые ключевые характеристики, благодаря которым графические процессоры – GPU отличаются от центральных процессоров – CPU:

- GPU являются ускорителями, дополняющими CPU, поэтому им не нужно уметь выполнять все задачи, присущие CPU. Эта роль позволяет им посвятить все свои ресурсы графике. Для GPU вполне нормально выполнять некоторые задачи недостаточно хорошо или вообще не выполнять, при условии, что в системе, где есть и CPU, и GPU. CPU может при необходимости эти задачи выполнить. Таким образом, комбинация CPU-GPU является одним из примеров гетерогенной мультипроцессорной обработки, где не все процессоры являются идентичными. (Еще одним примером может послужить архитектура IBM Cell которая также была разработана для ускорения 2D- и ЗD-графики)

- Интерфейсами программирования GPU являются высокоуровневые интерфейсы прикладного программирования (application programming interface, API), такие как OpenGL и разработанный компанией Microsoft DirectX, в совокупности с высокоуровневыми языками графического затенения, или шейдинга (shading), такими как разработанный компанией NVIDIA язык C для графики (Cg) и разработанный компанией Microsoft язык высокого уровня для программирования шейдеров High Level Shader language (HLSL). Компиляторы языков предназначены не для машинных инструкций, а для промежуточных языков, отвечающих промышленным стандартам. Программный драйвер GPU генерирует оптимизированные машинные инструкции, подходящие к конкретному GPU. Хотя эти API и языки развиваются довольно быстро, охватывая все новые GPU-ресурсы, допускаемые законом Мура, отсутствие необходимости соблюдения обратной совместимости по двоичным инструкциям позволяет GPU-разработчикам исследовать новые архитектуры, не испытывая опасения за постоянные неудачи с реализацией. Такая среда способствует более быстрому появлению нововведений в GPU, чем в CPU.

- Обработка графики предполагает рисование точек (вершин) трехмерных геометрических примитивов, таких как линии и треугольники, и затенение (shading) или прорисовку, или рендеринг (rendering), пиксельных фрагментов геометрических примитивов. В видеоиграх, к примеру, рисуется в 20-30 раз больше пикселов, чем вершин.

- Каждая точка (вершина) может быть нарисована независимо, и прорисовка каждого пиксельного фрагмента также может быть произведена независимо. Чтобы быстро прорисовать миллионы пикселов на кадр, создается GPU, способный параллельно выполнять множество потоков от программ, рисующих вершины и пиксельные затенения.

К типам графических данных относятся точки (вершины), состоящие из координат (х, у, z, w), и пикселы, состоящие из цветов (красный – red, зеленый – green, синий – blue, альфа alpha). Графические процессоры представляют каждый компонент точки в виде 32-разрядного числа с плавающей точкой. Каждый из четырех компонентов пикселов сначала был 8-разрядным беззнаковым целым числом, но новые графические процессоры теперь представляют каждый компонент в виде числа с плавающей точкой одинарной точности в диапазоне между 0,0 и 1,0.

Рабочий набор может иметь объем в сотни мегабайт, и он не отличается такой же локальностью, связанной со временем, которая присуща данным в обычных приложениях. Более того, графические задачи отличаются большим уровнем параллелизма данных.

Сравнение векторов и мультимедийных расширений

Как и мультимедийные расширения, имеющиеся в инструкциях x86 SSE, векторные инструкции определяют сразу несколько операций. Но мультимедийные расширения обычно определяют лишь небольшое их количество, в то время как векторные определяют десятки операций. В отличие от мультимедийных расширений, количество элементов в векторной операции определяется не в коде операции, а в отдельном регистре. Это означает, что разные версии векторной архитектуры могут быть реализованы с разным количеством элементов путем простого изменения содержимого этого регистра и, следовательно, сохранить двоичную совместимость. В отличие от этого, новый более крупный набор кодов операций всякий раз добавляет в мультимедийное расширение архитектуры x86 изменения длины вектора.

Также, в отличие от мультимедийных расширений, передача данных не нуждается в их смежности. Векторы поддерживают как пошаговые доступы, при которых оборудование загружает в память каждый n-ный элемент данных, так и индексированные доступы, при которых оборудование находит адреса загружаемых элементов в векторном регистре.

Как и мультимедийные расширения, вектор легко приспосабливается к гибкости в длине данных, поэтому заставить операции работать с 32 64-разрядными элементами данных, или с 64 32-разрядными элементами данных, или со 128 16-разрядными элементами данных, или с 256 8-разрядными элементами данных довольно просто.

В общем, векторные архитектуры являются довольно эффективным средством выполнения программ, занимающихся параллельной обработкой данных, они лучше соответствуют технологиям компилирования, нежели мультимедийные расширения, и им проще развиваться с течением времени, чем мультимедийным расширениям для архитектуры x86.

Уточнение. Почему же имеющие такие преимущества вектора не стали более популярными за пределами высокопроизводительных вычислений? Высказывались серьезные беспокойства насчет того, что векторные регистры увеличивают время переключения контекста, а также насчет сложности обработки ошибок отсутствия страниц при загрузках и сохранениях векторов, и утверждалось, что SIMD-инструкции достигли некоторых преимуществ векторных инструкций. Но в последних новостях компании Intel были намеки на то, что роль векторов будет расти. Технология INTEL под названием «усовершенствованные векторные инструкции» Advanced Vector Instructions (AVI) сможет мгновенно расширять SSE-регистры со 128 до 256 разрядов и допускать их расширение вплоть до 1024 разрядов. Последнее расширение эквивалентно 16 числам с плавающей точкой, имеющим двойную точность. Пока непонятно, будут ли при этом представлены инструкции загрузки и сохранения векторов. Кроме того, считается, что в процессоре Larrabee, проникновение Intel на рынок дискретных графических процессоров имеются векторные инструкции.

Уточнение. Еще одно преимущество векторов и мультимедийных расширений состоит в относительной простоте дополнения архитектуры набора скалярных инструкций инструкциями, использующими эти технологии для улучшения производительности операций параллельной обработки данных.

Сравнение векторных и скалярных инструкций

Векторные инструкции обладают рядом важных свойств по сравнению с обычной архитектурой набора инструкций, называемой в данном контексте скалярной архитектурой:

- Одной векторной инструкцией определяется большой объем работы, она эквивалентна выполнению целого цикла. Необходимый диапазон извлекаемых и декодируемых инструкций существенно сужается.

- За счет использования векторных инструкций компилятор или программист показывают, что вычисление каждого результата в векторе независимо от вычисления других результатов в этом же векторе, поэтому оборудованию не нужно вести проверку на наличие конфликтов данных внутри векторной инструкции.

- Векторные архитектуры и компиляторы прославились тем, что по сравнению с многопроцессорными MIMD-системами они позволяют существенно облегчить создание эффективных приложений при наличии параллелизма на уровне данных.

- Проверку на наличие конфликтов данных оборудованию нужно вести только лишь между двумя векторными инструкциями, и делать это для каждого векторного операнда, а не для каждого элемента внутри векторов. Сокращение проверок также ведет к экономии энергии.

- Векторные инструкции, обращающиеся к памяти, имеют известную схему обращения. Если все элементы векторов примыкают друг к другу, то извлечение вектора из набора банков памяти с развитым чередованием адресов работает очень эффективно. Поэтому потери на латентность при обращении к оперативной памяти возникают только один раз на вектор, а не на каждое слово в векторе.

- Поскольку весь цикл заменяется векторной инструкцией с предопределенным поведением, конфликты управления, которые, как правило, будут возникать из-за условных переходов в цикле, отсутствуют.

- Экономия на диапазоне инструкций и на проверке наличия конфликтов плюс эффективное использование диапазона памяти дают векторной архитектуре преимущества по сравнению со скалярной архитектурой в мощности и энергопотреблении.

По этим причинам векторные операции могут проводиться быстрее, чем последовательность скалярных операций с одинаковым количеством элементов данных, и разработчики имеют вполне серьезные основания для включения векторных блоков.

SIMD в х86: мультимедийные расширения

Параллелизм, достигаемый за счет работы с независимыми данными.

Наиболее широко используемый вариант SIMD-системы сегодня имеется практически в каждом микропроцессоре и составляет основу сотен MMX- и SSE-инструкций микропроцессора x86. Эти инструкции были добавлены для повышения производительности мультимедийных программ, и дают возможность оборудованию располагать множеством одновременно работающих АЛУ, или, что одно и то же, поделить одно широкое АЛУ на множество параллельных, меньших по размеру АЛУ, работающих одновременно. Например, один и тот же компонент оборудования можно рассматривать как одно 64-разрядное АЛУ, или как два 32-разрядных АЛУ, или как четыре 16-разрядных АЛУ, или как восемь 8-разрядных АЛУ. Загрузки и сохранения имеют просто такую же ширину, как и самое широкое АЛУ, поэтому программист может относиться к одним и тем же инструкциям передачи данных как к передаче 64-разрядных элементов данных, либо двух 32-разрядных элементов данных, либо четырех 16-разрядных элементов данных, либо восьми 8-разрядных элементов данных.

Этот очень дешевый параллелизм для ограниченных целочисленных данных послужил исходным толчком для создания инструкций ММХ семейства процессоров х86. Под влиянием закона Мура к этим мультимедийным расширениям было добавлено еще больше оборудования, и теперь инструкции SSE2 поддерживают одновременное выполнение действий сразу над двумя 64-разрядными числами с плавающей точкой.

Ширина операции и регистров закодирована в поле кода операции (opcode) этих мультимедийных инструкций. По мере расширения данных регистров и операций число кодов операций для мультимедийных инструкций стремительно растет, и теперь уже насчитываются сотни SSE-инструкций, позволяющих выполнять весьма полезные комбинации действий.

Вектор

Более старая и элегантная интерпретация SIMD называется векторной архитектурой, она тесно связана с продукцией компании Cray Computers. Здесь опять наблюдается довольно сильная связь с проблемами широкого применения параллелизма на уровне данных. Вместо использования 64 АЛУ, выполняющих одновременно 64 сложения, подобно старым матричным процессорам, векторная архитектура конвейеризирует АЛУ, чтобы получить высокую производительность при меньших затратах. Основная философия векторной архитектуры заключается в сборе элементов данных из памяти, помещении их в определенном порядке в большой набор регистров, проведении их последовательной обработки в регистрах, а затем в записи результатов обратно в память. Основным свойством векторных архитектур является набор векторных регистров. Поэтому векторная архитектура может иметь 32 векторных регистра, в каждом из которых 64 64-разрядных элемента.

Наиболее ярким является то, что векторный процессор существенно сокращает диапазон инструкций, выполняя только шесть инструкций против почти 600 для MIPS. Это сокращение получается из-за того, что векторные операции работают с 64 элементами, а также из-за того, что верхние команды, которые составляют на MIPS почта половину цикла, в векторном коде отсутствуют. Не удивительно, что это уменьшает количество извлекаемых инструкций и время их выполнения, экономя энергию.

В векторном процессоре каждая векторная инструкция будет приостанавливаться только для первого элемента каждого вектора, а затем последующие элементы будут ровно следовать вниз по конвейеру. Таким образом, задержки конвейера требуются только один раз для каждой векторной операции, а не один раз для каждого векторного элемента. В этом примере частота задержек конвейера в MIPS будет примерно в 64 раза выше, чем в VMIPS. Задержки конвейера могут быть сокращены в MIPS путем использования развертывания цикла. Но большую разницу в диапазоне инструкций сократить не удастся.

Уточнение. В предыдущем примере цикл точно совпадает с длиной вектора. Когда циклы короче, векторная архитектура использует регистр для ограничения длины векторной операции. Когда циклы длиннее, мы добавляем отслеживающий код, чтобы итерационно обработать всю векторную графику. Этот последний процесс называют разрывом итерации (strip mining).