ПрограммированиеСтатьиОбщее

Знакомство с MMX-технологией.

Автор: Сергей Забарянский


Сергей Забарянский, один из ведущих программистов недавно вышедшего шутера Venom, созданного GSC Game World, а теперь, один из основателей новой команды разработчиков Deep Shadows, делится своими соображениями по данной теме.

Какие же именно инструкции есть у популярного процессора?
Как организована работа с дополнительными типами данных?
И главное - зачем все это?
- Ответы на эти вопросы перед вами.

Призрак MMX уже давно бродит по страницам компьютерных изданий. Шуму вокруг него предостаточно. "MMX — прорыв в новое тысячелетие", — говорили нам страницы газет. А все почему? Из-за каких-то пятидесяти семи инструкций! И хотя цифра внушает нам трепет, все же не совсем ясно, как, не меняя кардинально архитектуру микропроцессора, можно получить супер-мультимедийный компьютер? За счет чего?

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

Допустим, у нас есть два массива: A и B, длиной по 8 байтов каждый. Поставим себе задачу — прибавить к каждому элементу массива B соответствующий ему элемент массива A. Это приведет нас к такой программе:

    mov  esi, offset A
    mov  edi, offset B
    mov  ecx, 8
OurLoop:
    mov  al, [esi]
    add  [edi], al
    inc  esi
    inc  edi
    loop OurLoop

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

    movq  mmreg1, A
    movq  mmreg2, B
    paddb mmreg2, mmreg1
    movq  B, mmreq2

Не правда ли просто и элегантно? Красиво ответите вы, однако какое отношение это все имеет к multimedia? Допустим, выполняется программа обрабатывающая 256-цветное изображение. Обычно она обрабатывает каждый пиксел по отдельности, используя же MMX, она может обрабатывать восемь пикселей сразу! Так что же такое MMX? MMX - это расширение, включающее в себя 57 новых команд и восемь 64-разрядных регистров. В основу положен принцип SIMD (Single Instruction Multiple Data) - одна инструкция - множество данных. MMX предоставляет инструкции для складывания, умножения и даже выполнения комбинированных операций. К примеру, команда PMADDWD перемножает, а затем складывает четыре слова данных, при этом она выполняется намного быстрее программы использующей только набор "стандартных" инструкций. Технология MMX предоставляет простую, гибкую программную модель, не требуя при этом перевода процессора в какой-то особенный режим. Все существующие программы будут корректно исполняться на процессорах с MMX без каких-либо изменений, даже если в системе присутствуют приложения, использующие новую технологию.

Регистры.

В процессорах использующих MMX добавлено 8 новых 64-разрядных регистров MM0-MM7. Они могут быть использованы только для выполнения операций с типами данных MMX. Команды MMX позволяют задавать в качестве операндов как регистры общего назначения (EAX, EBX, ECX, EDX, EBP, ESI, EDI и ESP), так и переменные в памяти, используя для этого стандартную схему адресации принятую в процессорах x86. Хотя MMX регистры и имеют ни с чем не совпадающие названия, на самом деле, они являются "псевдонимами" регистров сопроцессора (st0-st7). Это означает, что, изменяя один из регистров MMX, в своей программе, мы в то же время изменяем регистры сопроцессора. Зачем такие сложности? Не проще ли было их сделать "раздельными"? В конце концов, для этого понадобилось бы не так уж и много усилий со стороны разработчиков процессора... Очевидно, это было сделано не только с целью "добавить работу программистам". Как всегда, все дело в совместимости. Если бы было сделано так, как подсказывает логика, то пришлось бы переписать операционную систему! Те, кто знаком с работой процессора в защищенном режиме, наверное, уже догадались, о чем идет речь. Конечно же - о мультизадачности. Дело в том, что в многозадачной среде программы должны быть независимы друг от друга. При переключении с одной задачи на другую значения всех регистров "старой" задачи должны быть сохранены. В настоящее время о сохранении регистров микропроцессора заботится сам микропроцессор, а о сохранении регистров сопроцессора заботится операционная система. Так как все регистры MMX одновременно являют собой регистры FPU, то OS, сохраняя регистры FPU, сохраняет и регистры MMX! Проблема решена. Программист должен заботиться о том, чтобы код, использующий FPU и MMX, работал корректно. Код одного типа (MMX или FPU) должен быть по возможности сгруппирован. Для достижения наибольшей производительности в программе не должно быть условных переходов из части кода, использующего FPU к коду использующему MMX. После того как работа с MMX завершена, нужно очистить регистры mmreg0-mmreg7. Это делается для того, чтобы в последующем сопроцессор не столкнулся с "грязными" регистрами непонятного формата. Для этого используется новая инструкция EMMS, которая помечает все регистры FPU как "пустые".

Префиксы.

Всем инструкциям x86 ставится в соответствие число - код операции. Длинна кода операции один-два байта. Ему может предшествовать байт SIB (Scale-масштаб, Index-индекс, Base-база) определяющий полный режим адресации. В процессоре x86 перед инструкцией также возможно присутствие префиксов, несколько модифицирующих "смысл" команды (префикс замены сегмента, префикс команды). Инструкции MMX формируются точно также как и "старые" команды.

Определение присутствия.

Прежде чем начать использовать MMX, было бы неплохо убедиться в его присутствии. Если вам кажется, что это скучная задача, то вы ошибаетесь. Корпорация Intel делает все от нее зависящее, чтобы программисты отрабатывали свой хлеб. Решение, предлагаемое в документации до гениального просто. Достаточно вызвать инструкцию CPUID для получения флагов говорящих о том, чем напичкан процессор. Все бы было хорошо, если бы не одна тонкость. Для того чтобы ее вызвать, не получив при этом исключения (Exception), нужно убедиться в том, что программа выполняется на процессоре Pentium. Как это определить?

Судя по всему, должна быть команда, сообщающая информацию о типе процессора. Забавно, но у Intel ушло десять лет на то, чтобы реализовать такую инструкцию, хотя она должна была бы появиться еще в i8086. Одной из надежных эвристик определяющих тип процессора является проверка "устанавливаемости" 21 бита регистра флагов (EFLAGS). Чтобы не быть голословным, я написал программу mmxtest.asm (mmxtest.zip (919 байт)), которая проверяет присутствие MMX и не двусмысленно об этом сообщает.

Распаровка.

Точно также как и "обычные" инструкции микропроцессора инструкции MMX могут выполняться в паре с другой инструкцией одновременно. Более того, в некоторых случаях, возможно, одновременное выполнение двух инструкций MMX. Операции сдвига и умножения могут быть выполнены как на u-, так и на v- конвейере. Ограничения, накладываемые на возможность распаровки следующие:

  • Две инструкции, использующие блок сдвигов MMX (инструкции упаковки/распаковки и сдвига) не могут выполняться одновременно.
  • Две инструкции, использующие блок умножения MMX (инструкции типа pmul, pmulh, pmadd) не могут выполняться в паре.
  • Инструкции MMX, которые обращаются к памяти, могут выполняться только на u- конвейере.
  • Целевой регистр MMX-инструкции попавшей в u-конвейер не должен совпадать с регистром-источником инструкции попавшей на v-конвейер (тест на зависимость). То есть пара инструкций:
  •   paddb  mmreg2, mmreg1
      paddb  mmreg3, mmreg2

    *Инструкция EMMS не паруется.

  • Если или CR0.TS, или CR0.EM установлены, то MMX инструкции не могут быть выполненными на v-конвейере.
  • ТИПЫ ДАННЫХ.

    MMX поддерживает данные в упакованном формате. Это означает, что каждый 64 битовый регистр MMX может интерпретироваться как:

    То, как интерпретируется конкретный регистр, отлично отображается в мнемонике команды. К примеру, инструкция PADDB складывает два операнда, полагая, что они являются набором байт, а PADDW считает операнды набором слов.

    Все инструкции можно разбить на следующие группы:

    арифметические команды
      сложение PADDB PADDD PADDSB PADDSW PADDUSB PADDUSW PADDW
      сложение и умножение PMADDWD
      умножение PMULHW PMULLW
      вычитание PSUBB PSUBD PSUBSB PSUBSW PSUBUSB PSUBUSW PSUBW
    команды очищающие регистры
      EMMS
    команды сравнения
      проверка равенства PCMPEQB PCMPEQD PCMPEQW
      сравнение PCMPGTB PCMPGTD PCMPGTW
    команды упаковки/распаковки
      PUNPCKHBW PUNPCKHDQ PUNPCKHWD PUNPCKLBW PUNPCKLDQ PUNPCKLWD PACKSSDW PACKSSWB PACKUSWB
    логические команды
      PAND PANDN POR PXOR
    команды передачи данных
      MOVD MOVQ
    команды сдвига
      логический сдвиг влево PSLLD PSLLQ PSLLW
      арифметический сдвиг вправо PSRAD PSRAW
      логический сдвиг вправо PSRLD PSRLQ PSRLW

    Команды MMX имеют такой формат:

    инструкция mmreg1, mmreg2/mem64

    То есть источником может быть как переменная памяти, так и регистр MMX. А целевым может быть только регистр MMX. К тому же MOVD и MOVQ допускают пересылки из регистров MMX в память. Одним из первых вопросов у меня был: "зачем так много команд?". Ведь количество операций не так уж и велико... Дело в том, что название команды формируется из двух частей. Первая часть говорит о том, что она делает (MOV,PSUB,PADD). Вторая же говорит о том, как она интерпретирует операнды. Рассмотрим команду PACKUSWB. Первая часть строки - PACK указывает, что будем что-то упаковывать. Вторая же - USWB в свою очередь разбивается на две US и WB. US говорит о том, что результат будет беззнаковым с сатурацией , WB же указывает, что источник - упакованные слова, результат - упакованные байты. Благодаря такой записи смысл инструкции схватывается "на лету", по этой же причине команд так много. Приведем полный перечень мнемоник:

    О смысле жизни.

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

    Вообще существует множество задач, где MMX приходится весьма кстати. К примеру, MMX прекрасно подходит для задач обработки звука. Действительно, оцифрованный звук задается последовательностью 8-ми,16-ти или 32-разрядных чисел. По этой причине, если нам нужно, к примеру, воспроизвести два звука одновременно, то согласно теореме о суперпозиции нам следует сложить соответствующие последовательности чисел. Тут MMX на высоте. Кроме того, как нельзя кстати, есть возможность складывать с сатурацией. В результате скорость может быть повышена в 4-5 раз.

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

    Правда можно в качестве элементов использовать числа с фиксированной точкой . Размер такого числа - четыре байта. То есть в один регистр поместится только два элемента вектора. Тонкость тут в том, что в случае трехмерных преобразований скалярное произведение нужно считать от двух трехэлементных векторов... Решить проблему можно - использовать для этого два MMX регистра: в первом два элемента, во втором один. Однако выигрыш при этом будет не столь значителен, как хотелось бы.

    К чему же мы пришли?

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

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

    КРАТКОЕ ОПИСАНИЕ КОМАНД.

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

    MOVD
    Формат
      MOVD mmreg1, reg32/mem32
      MOVD reg32/mem32, mmreg1
    Описание
    Инструкция MOVD пересылает 32 младших бита регистра MMX в регистр общего назначения или память. Или же из регистра общего назначения/памяти в регистр MMX. В последнем случае кроме собственно пересылки биты 32-64 соответствующего регистра обнуляются.

    PADDB
    Формат
      PADDB mmreg1, mmreg2/mem64
    Описание
    Складывает восемь 8-разрядных чисел операнда источника (MMX регистра или 64-разрядного поля памяти) и восемь 8-разрядных чисел операнда-получателя (регистра MMX). В случае переполнения полученные значения заворачиваются (то есть 255+10=9) без установки флага переноса при подсчете последующих байтов. Результат помещается в операнд-получатель.

    PADDSW
    Формат
      PADDSW mmreg1, mmreg2/mem64
    Описание
    Складывает четыре 16-битных чисел со знаком операнда источника (регистра MMX или 64-разрядного поля памяти) и четыре соответствующих значения операнда-получателя (регистра MMX). Если сумма каких-либо двух из них менее чем -32768 (8000h), то результат сложения -32768 (8000h). Аналогично в случае, когда сумма превышает 32767 (7FFFh), то возвращаемый результат 32767 (7FFFh). Результат заносится в операнд-получатель.Следующий список возможных ситуаций помогает понять, как происходит сложение:

    Если число D250h (-11696) складывается с 8807h (-30713), то получаемый результат 8000h (-32768) - минимально возможное 16-разрядное число.
    Если число 5321h (+21281) прибавляется в EC22h (-5086), то получаемый результат 3F43h (+16195).
    Сумма 16-разрядного числа 7007h (+28679) и 0FF9h (+4098) дает результат 7FFFh (+32767) - максимально возможное положительное число.
    Если к FFFFh (-1) прибавить FFFFh (-1), то получим FFFEh (-2). 

    PAND
    Формат
      PAND mmreg1, mmreg2/mem64
    Описание
    Инструкция PAND выполняет логическую операцию "и" над операндами. Результат заносится в операнд-получатель. Если соответствующие биты источника и получателя равны единице, то значение итогового бита единица. Эта команда может использоваться для распаковки упакованных переменных при помощи маски полученной от инструкций сравнения PCMPEQ и PCMPGT.

    PCMPEQB
    Формат
      PCMPEQB mmreg1, mmreg2/mem64
    Описание
    Сравнивает источник с получателем, рассматривая операнды как упакованные байты. Если биты операндов эквивалентны, то все биты 8-разрядной части получателя устанавливаются равными единице, в противном случае они обнуляются.

    PCMPGTD
    Формат
      PCMPGTB mmreg1, mmreg2/mem64
    Описание
    Аналогична PCMPEQ, но в отличие от нее биты в целевом операнде устанавливается в том случае, когда байт целевого операнда больше соответствующего байта операнда источника.

    PACKUSWB
    Формат
      PACKUSWB mmreg1, mmreg2/mem64
    Описание
    Переводит восемь знаковых слов задаваемые аргументами (по четыре слова в каждом) в восемь беззнаковых байт. После чего результат сохраняется в mmreg1. Сатурация при этом происходит следующим образом:

  • Если слово содержит отрицательное число, то соответствующий ему байт будет равен нулю
  • Если значение слова превышает 255, то соответствующий ему байт будет равен 255
  • #MMX

    31 октября 2001