Ускорение ассемблера x64 Добавление цикла

Я работаю над арифметикой умножения очень длинных целых чисел (около 100 000 десятичных цифр). В рамках моей библиотеки я добавил два длинных числа.

Профилирование показывает, что мой код работает до 25% своего времени в процедурах add() и sub(), поэтому важно, чтобы они выполнялись как можно быстрее. Но я пока не вижу большого потенциала. Может быть, вы можете дать мне некоторую помощь, совет, понимание или идеи. Я протестирую их и вернусь к вам.

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

mov rax, QWORD PTR [rdx+r11*8-64]
mov r10, QWORD PTR [r8+r11*8-64]
adc rax, r10
mov QWORD PTR [rcx+r11*8-64], rax

Далее следуют еще 7 блоков с разными смещениями, а затем цикл.

Раньше я пытался загрузить значения из памяти, но это не помогло. Я думаю, это из-за хорошей предварительной выборки. Я использую 4-ядерный процессор Intel i7-3770 Ivy Bridge. Но я хотел бы написать код, который хорошо работает на любом современном процессоре.

Редактировать: я сделал некоторые тайминги: он добавляет 1 тыс. слов примерно за 2,25 цикла на слово. Если я уберу АЦП, и останутся только MOV, это все равно займет около 1,95 цикла на слово. Таким образом, основным узким местом является доступ к памяти. Библиотека memcpy() работает примерно за 0,65 цикла/слово, но имеет только один вход, а не два. Тем не менее, я думаю, он намного быстрее из-за использования регистров SSE.

Некоторые вопросы:

  • Полезно ли использовать структуру «загрузка, загрузка, добавление, сохранение» или поможет «загрузка, добавление в память»? Пока мои тесты не показали никаких преимуществ.
  • Как обычно, помощи от SSE(2,3,4) ждать не приходится?
  • Плохо ли влияет адресация (масштабированный индекс плюс база плюс смещение)? Вместо этого я мог бы использовать ADD r11, 8.
  • А как насчет разворачивания цикла? Я читал, что развертывание плохо для архитектуры Sandy Bridge (Agner Fog http://www.agner.org/optimize/). Его следует предпочесть или избегать?
  • (Правка) Могу ли я использовать регистры SSE для загрузки и хранения слов большими порциями из памяти и эффективного обмена словами с помощью регистров общего назначения и регистров SSE?

Я высоко ценю любые комментарии.


person cxxl    schedule 20.12.2012    source источник
comment
Самый быстрый способ (я знаю) умножить очень большое число — это быстрое преобразование Фурье en.wikipedia.org/wiki /Multiplication_algorithm Я никогда не пытался реализовать его логику на ассемблере. По-видимому, Prime95 содержит быстрое преобразование Фурье в логике сборки x86, и вы можете взять его (свободно) оттуда.   -  person Benjamin Gruenbaum    schedule 20.12.2012
comment
Спасибо, я знаю об этом. Прямо сейчас я хочу только добавить быстро.   -  person cxxl    schedule 20.12.2012
comment
Вы можете посмотреть в источниках GMP.   -  person zch    schedule 20.12.2012
comment
Попробуйте запустить IACA, посмотрите, даст ли он полезный результат: software.intel.com/ru-ru/articles/   -  person Necrolis    schedule 20.12.2012
comment
IACA кажется крутым, но документации в лучшем случае мало. Мне придется потратить еще немного времени, чтобы понять это. Любые ресурсы?   -  person cxxl    schedule 21.12.2012


Ответы (3)


Я почти уверен, что memcpy быстрее, потому что он не зависит от извлекаемых данных, прежде чем он сможет выполнить следующую операцию.

Если вы можете организовать свой код так, чтобы он делал что-то вроде этого:

mov rax, QWORD PTR [rdx+r11*8-64]
mov rbx, QWORD PTR [rdx+r11*8-56]
mov r10, QWORD PTR [r8+r11*8-64]
mov r12, QWORD PTR [r8+r11*8-56]
adc rax, r10
adc rbx, r12
mov QWORD PTR [rcx+r11*8-64], rax
mov QWORD PTR [rcx+r11*8-56], rbx

Я не уверен на 100%, что смещение -56 подходит для вашего кода, но концепция "правильная".

Я бы также рассмотрел кэш-попадания/кеш-коллизии. Например. если у вас есть три блока данных [что, казалось бы, у вас есть], вы убедитесь, что они НЕ выровнены по одному и тому же смещению в кеше. Плохим примером может быть, если вы выделяете все свои блоки в размере, кратном размеру кеша, из одного и того же места в кеше. Перераспределение и УБЕДЕНИЕ, что ваши различные блоки данных смещены не менее чем на 512 байт [поэтому выделите 4 КБ сверхразмера и округлите до 4 КБ граничный начальный адрес, затем добавьте 512 во второй буфер и 1024 в третий буфер]

Если ваши данные достаточно велики (больше, чем кеш L2), вы можете использовать MOVNT для извлечения/сохранения ваших данных. Это позволит избежать чтения в кеш - это ТОЛЬКО полезно, когда у вас очень большие данные, когда следующее чтение просто приведет к тому, что что-то еще, что вы можете найти «полезным», будет удалено из кеша, и вы не получите вернуться к нему до того, как вы выкинули его из кеша, поэтому сохранение значения в кеше на самом деле не поможет...

Изменить: использование SSE или аналогичного не поможет, как описано здесь: Может быть длинное целое подпрограммы выигрывают от SSE?

person Mats Petersson    schedule 20.12.2012

Самая сложная зависимость — распространение переноса между каждым блоком памяти; Я бы попытался сначала найти способ справиться с этим.

Следующий фрагмент имитирует распространение переноса, но с «преимуществом» в том, что флаг переноса не используется. Это можно распараллелить для трех или четырех отдельных потоков, каждый из которых производит половину, переносящую около 25 000 десятичных цифр (или 10 000 байтов) друг от друга. Тогда вероятность того, что эти переносы повлияют только на один байт, слово, двойное слово и т. д., асимптотически достигнет нуля.

 long long carry=0;
 for (int i=0;i<N;i++) {
     carry += (long long)*a++ + (long long)*b++;
     *c++ = carry; carry>>=32;
 }

Согласно моему профилированию, сложение без переноса с использованием xmm займет ~ 550 мс (1e9 слов), симуляция переноса займет ~ 1020 мс, а 4-сторонняя параллельная версия займет ~ 820 мс (без какой-либо оптимизации ассемблера).

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

person Aki Suihkonen    schedule 20.12.2012

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

Вы также можете попытаться сократить свой код до:

mov rax, QWORD PTR [rdx+r11*8-64]
adc rax, QWORD PTR [r8+r11*8-64]
mov QWORD PTR [rcx+r11*8-64], rax
person Bartosz Wójcik    schedule 20.12.2012
comment
Я пробовал все это и не получил никакой пользы, иногда даже худшие тайминги. Основное время, кажется, теряется в доступе к памяти. - person cxxl; 21.12.2012