Советы и рекомендации по программированию процессоров Intel® Pentium® 4

Дата публикации 30 июн 2006

Советы и рекомендации по программированию процессоров Intel® Pentium® 4 — Архив WASM.RU

Введение

Многие считают, что для повышения производительности программ, выполняющихся на процессорах Intel® Pentium® 4, нужно использовать инструкции Streaming SIMD Extensions (SSE) и Streaming SIMD Extensions 2 (SSE2). В большинстве случаев так оно и есть. Действительно, с помощью инструкций SSE2 и 128-битных регистров XMM обычно можно значительно повысить производительность приложения. Но с другой стороны, если программа использует 64-битные данные, и их упаковка в 128-битные регистры не повышает эффективность, то стоит не тратить время на SSE или SSE2, а поискать другие способы оптимизации.

Использование SIMD инструкций и 128-битных регистров может заметно увеличить скорость программы. Но что делать, если мы можем работать лишь с 64-битными данными? Если у нас только 64-битные данные, это ещё не значит, что нельзя использовать 128-битные регистры XMM. Можно попробовать переупорядочить переменные и сложить их в те же регистры.

Но проблема в том, что затраты времени на перегруппировку могут превзойти любое ускорение. С другой стороны, стоит подумать о технологии Hyper-Threading. Системы, в которых используется эта технология, отличаются от многопроцессорных вариантов тем, что такие физические ресурсы, как кэш и исполнительные модули (execution units) становятся общими для логических процессоров.

Оптимизация для процессоров Pentium 4 с технологией Hyper-Threading

Чтобы оптимизировать приложение для выполнения на процессоре Pentium 4, можно использовать или MMX, или SSE/SSE2 инструкции. Вопрос в том, когда стоит использовать MMX™, а когда SSE/SSE2. Обычно сначала пробуют инструкции SSE/SSE2, потому что они работают с регистрами XMM, которые могут хранить 128 бит, в отличие от 64-битных регистров MMX.

Хотя регистры XMM более вместительные, некоторые инструкции гораздо дольше выполняются с регистрами XMM, несмотря на то, что задействован тот же самый исполнительный модуль. Например, инструкция paddq выполняется за шесть тактов на регистрах XMM, но только за два такта - на MMX.

Поэтому, если приложение выполняет множество расчетов с данными величиной 128 бит, то SSE/SSE2 стоит попробовать. В противном случае очень много времени будет уходить на перетасовку данных в XMM, и от этих задержек пропадёт всякое преимущество использования SSE/SSE2 вместо MMX.

Иногда есть смысл использовать сочетание инструкций MMX и SSE/SSE2. В регистрах XMM можно хранить дополнительные данные, если MMX операциям требуется временное хранилище для больших объёмов данных. В таком случае не надо подгружать определённые значения или ждать, пока освободятся регистры MMX для загрузки новых данных. Так можно сэкономить значительное количество тактов на перемещении переменных.

Первым делом проанализируйте и постарайтесь упростить свои подпрограммы и функции. Иногда перестройка условий и ветвления может дать значительный прирост по скорости. Может быть, из цикла можно убрать одно условие и поставить его в начале цикла. Так будет удалён один условный переход из цикла, который выполняется 640 раз (по количеству пикселей в строке развёртки при разрешении 640Х480). А если эта функция вызывается в программе много раз, то оптимизация становится ещё более ощутимой.

Всем известно, что копирование данных из памяти в регистры или в другое место памяти может занять много времени, если кодируется непродуманно. Обычно данные пытаются загрузить задолго до их использования. Ещё можно поискать эквивалент инструкции с более коротким временем выполнения. Например, возьмите инструкцию pshufw (два такта) вместо movd (шесть тактов) для перемещения значений между регистрами MMX (с pshufw ставьте константу 0xE4 для прямого копирования).

Примечание переводчика: Здесь, по-видимому, вместо инструкции movd, автор подразумевал movq, которая действительно занимает 6 тактов на P3.

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

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

Применение рекомендаций

Далее показано применение некоторых советов на практике.

Инициализация данных:

- Установка регистра в ноль:

Код (Text):
  1.  
  2. movd eax, 0
  3. xor eax, eax
  4. pxor mm0, mm0
  5. pxor xmm0, xmm0

- Установить все биты MM0 в единицу:

Код (Text):
  1.  
  2. C declaration: unsigned temp[4] = {0xFFFFFFFF, 0xFFFFFFFF,
  3. 0xFFFFFFFF, 0xFFFFFFFF};
  4.  
  5. asm { movq mm0, temp
  6. movdq xmm1, temp}

А так быстрее:

Код (Text):
  1.  
  2. pcmpeqd mm0, mm0
  3. pcmpeqd xmm1, xmm1

Готовим константы:

- Загрузить число 0xFF00FF00FF00FF00 в mm7:

Код (Text):
  1.  
  2. pcmpeqd mm7, mm7 // 0xFF FF FF FF FF FF FF FF
  3. psllq mm7, 8 // 0xFF FF FF FF FF FF FF 00
  4. pshufw mm7, mm7, 0x0 // 0xFF 00 FF 00 FF 00 FF 00

Каждая инструкция выполняется за 2 такта. Весь код будет выполнен за шесть тактов.

А вот быстрее:

Код (Text):
  1.  
  2. pxor mm7, mm7 // 0x 0
  3. pcmpeqd mm0, mm0 // 0x FFFFFFFFFFFFFFFF
  4. punpcklbw mm7, mm0 // 0x FF00FF00FF00FF00

В этом случае и pxor, и pcmpeqd обрабатываются в исполняющем модуле MMX-ALU, а punpcklbw - MMX-SHIFT. Каждая инструкция выполняется за два такта, но MMX-ALU ждёт только один такт, а не дожидается завершения b, прежде чем загрузить инструкцию pcmpeqd. Таким образом, все три инструкции выполняются за пять тактов, а не за шесть.

- Загрузить число 0x00FF00FF00FF00FF в mm7:

Код (Text):
  1.  
  2. pxor mm0, mm0 // 0x 0
  3. pcmpeqd mm7, mm7 // 0x FFFFFFFFFFFFFFFF
  4. punpcklbw mm7, mm0 // 0x 00FF00FF00FF00FF

Примечание: такой же принцип можно применить и к регистрам XMM с небольшими изменениями, так как в один момент времени мы можем работать лишь с одной половиной регистра XMM.

Примечание переводчика: Если есть SSE2, то можно все 128 бит сразу обработать точно такими же инструкциями, только с использованием регистров XMM.

Загрузка данных:

Код (Text):
  1.  
  2. movq mm1, mm2

А так быстрее:

Код (Text):
  1.  
  2. pshufw mm1, mm2, 0xE4

Примечание: хитрость здесь в особом числе 0xE4: оно значит, что порядок слов в числе менять не надо.

Это хороший вариант для копирования одного регистра в другой. Инструкция movq выполняется за шесть тактов, тогда как для pshufw требуется только два. Но не стоит бездумно заменять все movq на pshufw. Соответствующий модуль исполнения должен быть свободен в это время. Инструкции movq и pshufw выполняются в модулях FP_MOV и MMX_SHFT соответственно.

Обмен частей данных:

- Поменять верхнюю и нижнюю половину регистра местами:

Код (Text):
  1.  
  2. pshufw mm0, mm0, 0x4E
  3. pshufd xmm0, xmm0, 0x4E

Примечание: Если записать число 0x4E наоборот, то получится 0xE4, т.е. операция прямого копирования, а не обмена половин местами.

- Формирование комбинаций:

Записать 0xAADDAADDAADDAADD в регистр mm0:

Код (Text):
  1.  
  2. movd eax, 0xAADD
  3. movd mm0, eax
  4. pshufw mm0, mm0, 0x0

Примечание: Константа 0x0 скопирует содержимое первого слова в регистре, т.е. "AADD", во все остальные слова регистра mm0.

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

Примечание переводчика: Если есть SSE2, то достаточно выполнить эту последовательность лишь один раз, а потом использовать pshufd с константой 0X44, чтобы нижняя половина регистра была скопирована в верхнюю. Вот как это будет выглядеть:

Код (Text):
  1.  
  2.     movd        eax,0XAADD
  3.     movd        xmm0,eax
  4.     pshuflw xmm0,xmm0,0X0
  5.     pshufd  xmm0,xmm0,0X44

Использование инструкции lea:

Код (Text):
  1.  
  2. mov edx,ecx
  3. sal edx,3

А вот быстрее:

Код (Text):
  1.  
  2. lea edx, [ecx + ecx]
  3. add edx, edx
  4. add edx, edx

Примечание: Инструкция lea с двумя сложениями add получается быстрее, но нельзя ставить после неё более трёх add, иначе производительность растёт и превышает ускорение от lea.

Заключение

Не всегда использование инструкций SSE/SSE2 ускорит работу приложения на Pentium 4. Если вычисление идёт над целочисленными значениями, упакованными в 64 бита, то вместо SSE/SSE2 лучше использовать MMX.

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

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

Исходная статья на английском языке находится на сайте intel.com. © Ханг Нгуен, пер. SolidCode


0 1.337
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532