Улучшенный REP MOVSB для memcpy
Я хотел бы использовать расширенный REP MOVSB (ERMSB), чтобы получить высокую пропускную способность для пользовательских memcpy
,
ERMSB был представлен микроархитектурой Ivy Bridge. См. Раздел "Расширенные операции REP MOVSB и STOSB (ERMSB)" в руководстве по оптимизации Intel, если вы не знаете, что такое ERMSB.
Единственный способ, которым я знаю, сделать это напрямую - это встроенная сборка. Я получил следующую функцию из https://groups.google.com/forum/
static inline void *__movsb(void *d, const void *s, size_t n) {
asm volatile ("rep movsb"
: "=D" (d),
"=S" (s),
"=c" (n)
: "0" (d),
"1" (s),
"2" (n)
: "memory");
return d;
}
Когда я использую это, однако, пропускная способность намного меньше, чем с memcpy
, __movsb
получает 15 ГБ / с и memcpy
получить 26 ГБ / с с моей системой i7-6700HQ (Skylake), Ubuntu 16.10, двухканальный DDR4@2400 МГц, 32 ГБ, GCC 6.2.
Почему пропускная способность намного ниже с REP MOVSB
? Что я могу сделать, чтобы улучшить это?
Вот код, который я использовал для проверки этого.
//gcc -O3 -march=native -fopenmp foo.c
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stddef.h>
#include <omp.h>
#include <x86intrin.h>
static inline void *__movsb(void *d, const void *s, size_t n) {
asm volatile ("rep movsb"
: "=D" (d),
"=S" (s),
"=c" (n)
: "0" (d),
"1" (s),
"2" (n)
: "memory");
return d;
}
int main(void) {
int n = 1<<30;
//char *a = malloc(n), *b = malloc(n);
char *a = _mm_malloc(n,4096), *b = _mm_malloc(n,4096);
memset(a,2,n), memset(b,1,n);
__movsb(b,a,n);
printf("%d\n", memcmp(b,a,n));
double dtime;
dtime = -omp_get_wtime();
for(int i=0; i<10; i++) __movsb(b,a,n);
dtime += omp_get_wtime();
printf("dtime %f, %.2f GB/s\n", dtime, 2.0*10*1E-9*n/dtime);
dtime = -omp_get_wtime();
for(int i=0; i<10; i++) memcpy(b,a,n);
dtime += omp_get_wtime();
printf("dtime %f, %.2f GB/s\n", dtime, 2.0*10*1E-9*n/dtime);
}
Причина, по которой я заинтересован rep movsb
основан на этих комментариях
Обратите внимание, что в Ivybridge и Haswell с буферами большого размера, которые можно уместить в MLC, вы можете побить movntdqa с помощью rep movsb; movntdqa переносит RFO в LLC, rep movsb не... rep movsb значительно быстрее чем movntdqa при потоковой передаче в память на Ivybridge и Haswell (но имейте в виду, что до Ivybridge это медленно!)
Чего не хватает / неоптимально в этой реализации memcpy?
Вот мои результаты на той же системе от tinymembnech.
C copy backwards : 7910.6 MB/s (1.4%)
C copy backwards (32 byte blocks) : 7696.6 MB/s (0.9%)
C copy backwards (64 byte blocks) : 7679.5 MB/s (0.7%)
C copy : 8811.0 MB/s (1.2%)
C copy prefetched (32 bytes step) : 9328.4 MB/s (0.5%)
C copy prefetched (64 bytes step) : 9355.1 MB/s (0.6%)
C 2-pass copy : 6474.3 MB/s (1.3%)
C 2-pass copy prefetched (32 bytes step) : 7072.9 MB/s (1.2%)
C 2-pass copy prefetched (64 bytes step) : 7065.2 MB/s (0.8%)
C fill : 14426.0 MB/s (1.5%)
C fill (shuffle within 16 byte blocks) : 14198.0 MB/s (1.1%)
C fill (shuffle within 32 byte blocks) : 14422.0 MB/s (1.7%)
C fill (shuffle within 64 byte blocks) : 14178.3 MB/s (1.0%)
---
standard memcpy : 12784.4 MB/s (1.9%)
standard memset : 30630.3 MB/s (1.1%)
---
MOVSB copy : 8712.0 MB/s (2.0%)
MOVSD copy : 8712.7 MB/s (1.9%)
SSE2 copy : 8952.2 MB/s (0.7%)
SSE2 nontemporal copy : 12538.2 MB/s (0.8%)
SSE2 copy prefetched (32 bytes step) : 9553.6 MB/s (0.8%)
SSE2 copy prefetched (64 bytes step) : 9458.5 MB/s (0.5%)
SSE2 nontemporal copy prefetched (32 bytes step) : 13103.2 MB/s (0.7%)
SSE2 nontemporal copy prefetched (64 bytes step) : 13179.1 MB/s (0.9%)
SSE2 2-pass copy : 7250.6 MB/s (0.7%)
SSE2 2-pass copy prefetched (32 bytes step) : 7437.8 MB/s (0.6%)
SSE2 2-pass copy prefetched (64 bytes step) : 7498.2 MB/s (0.9%)
SSE2 2-pass nontemporal copy : 3776.6 MB/s (1.4%)
SSE2 fill : 14701.3 MB/s (1.6%)
SSE2 nontemporal fill : 34188.3 MB/s (0.8%)
Обратите внимание, что в моей системе SSE2 copy prefetched
также быстрее, чем MOVSB copy
,
В моих оригинальных тестах я не отключал турбо. Я отключил турбо и протестировал снова, и это, кажется, не имеет большого значения. Однако изменение управления питанием действительно имеет большое значение.
Когда я делаю
sudo cpufreq-set -r -g performance
Иногда я вижу более 20 ГБ / с rep movsb
,
с
sudo cpufreq-set -r -g powersave
лучшее, что я вижу, составляет около 17 ГБ / с. Но memcpy
не кажется чувствительным к управлению питанием.
Я проверил частоту (используя turbostat
) с включенным SpeedStep и без него, с performance
и с powersave
для холостого хода - нагрузка на 1 ядро и нагрузка на 4 ядра. Я запустил умножение плотной матрицы Intel MKL, чтобы создать нагрузку и установить количество потоков, используя OMP_SET_NUM_THREADS
, Вот таблица результатов (цифры в ГГц).
SpeedStep idle 1 core 4 core
powersave OFF 0.8 2.6 2.6
performance OFF 2.6 2.6 2.6
powersave ON 0.8 3.5 3.1
performance ON 3.5 3.5 3.1
Это показывает, что с powersave
даже если SpeedStep отключен, процессор продолжает работать на частоте холостого хода 0.8 GHz
, Это только с performance
без SpeedStep, что процессор работает с постоянной частотой.
Я использовал например sudo cpufreq-set -r performance
(так как cpufreq-set
давал странные результаты) поменять настройки питания. Это снова включает турбо, поэтому мне пришлось отключить турбо после.
6 ответов
Эта тема очень близка моему сердцу и недавним исследованиям, поэтому я буду смотреть на нее с нескольких сторон: история, некоторые технические заметки (в основном академические), результаты тестов на моей коробке и, наконец, попытка ответить на ваш актуальный вопрос когда и где rep movsb
может иметь смысл.
Частично, это вызов для обмена результатами - если вы можете запустить Tinymembench и поделиться результатами вместе с подробной информацией о конфигурации вашего ЦП и ОЗУ, это было бы здорово. Особенно, если у вас есть 4-канальная установка, блок Ivy Bridge, серверный блок и т. Д.
История и официальные советы
История выполнения инструкций по быстрому копированию строк была чем-то вроде ступеньки - то есть периоды неизменной производительности чередовались с большими обновлениями, которые приводили их в соответствие или даже быстрее, чем конкурирующие подходы. Например, произошел скачок производительности в Nehalem (в основном с целью увеличения накладных расходов при запуске) и снова в Ivy Bridge (большая часть была нацелена на общую пропускную способность для больших копий). Вы можете найти десятилетнее понимание трудностей реализации rep movs
инструкции от инженера Intel в этой теме.
Например, в руководствах, предшествующих введению Ivy Bridge, типичный совет - избегать их или использовать их очень осторожно 1.
В текущем (ну, июнь 2016 г.) руководстве содержатся различные запутанные и несколько непоследовательные советы, например 2:
Конкретный вариант осуществления выбирается во время выполнения на основе расположения данных, выравнивания и значения счетчика (ECX). Например, MOVSB /STOSB с префиксом REP следует использовать со значением счетчика, меньшим или равным трем, для лучшей производительности.
Так для копий 3 или менее байтов? Вам не нужен rep
Во-первых, это префикс для этого, так как с заявленной задержкой запуска ~9 циклов вам почти наверняка лучше с простым DWORD или QWORD mov
с небольшим переворотом, чтобы скрыть неиспользуемые байты (или, возможно, с 2 явными байтами, словом mov
s, если вы знаете, что размер ровно три).
Они продолжают говорить:
Строковые инструкции MOVE/STORE имеют несколько гранулярностей данных. Для эффективного перемещения данных предпочтительнее большая степень детализации данных. Это означает, что лучшая эффективность может быть достигнута путем разложения произвольного значения счетчика на количество двойных слов плюс однобайтовые перемещения со значением счетчика, меньшим или равным 3.
Это, конечно, кажется неправильным на текущем оборудовании с ERMSB, где rep movsb
по крайней мере так же быстро или быстрее, чем movd
или же movq
варианты для больших копий.
В целом, этот раздел (3.7.5) настоящего руководства содержит сочетание разумных и сильно устаревших советов. Это обычная пропускная способность руководств Intel, поскольку они обновляются поэтапно для каждой архитектуры (и имеют целью охватить архитектуры почти двух десятилетий даже в текущем руководстве), а старые разделы часто не обновляются, чтобы заменить или дать условную рекомендацию. это не относится к текущей архитектуре.
Затем они подробно освещают ERMSB в разделе 3.7.6.
Я не буду подробно останавливаться на оставшемся совете, но суммирую хорошие моменты в разделе "зачем его использовать" ниже.
Другие важные претензии из руководства, что на Haswell, rep movsb
был расширен для использования 256-битных операций внутри.
Технические соображения
Это просто краткое изложение основных преимуществ и недостатков, которые rep
инструкции имеют с точки зрения реализации.
Преимущества для rep movs
Когда
rep
Инструкция MOVS выдается, процессор знает, что должен быть передан весь блок известного размера. Это может помочь ему оптимизировать работу так, как это невозможно с дискретными инструкциями, например:- Избегание запроса RFO, когда он знает, что вся строка кэша будет перезаписана.
- Выдача запросов на предварительную выборку немедленно и точно. Аппаратная предварительная выборка хорошо помогает при обнаружении
memcpy
-подобные шаблоны, но для запуска все равно требуется несколько операций чтения, что приведет к "чрезмерной предварительной загрузке" многих строк кэша за пределами конца скопированной области.rep movsb
точно знает размер региона и может точно выполнить предварительную выборку.
По всей видимости, нет гарантии заказа среди магазинов в течение 3-х раз
rep movs
что может помочь упростить трафик когерентности и просто другие аспекты перемещения блока, по сравнению с простымmov
инструкции, которые должны подчиняться довольно строгому порядку памяти 4.В принципе
rep movs
инструкция может использовать различные архитектурные приемы, которые не представлены в ISA. Например, архитектуры могут иметь более широкие внутренние пути данных, которые выставляет ISA 5 иrep movs
мог бы использовать это внутренне.
Недостатки
rep movsb
должен реализовывать определенную семантику, которая может быть сильнее, чем базовое требование к программному обеспечению. Особенно,memcpy
запрещает перекрывающиеся регионы, и поэтому может игнорировать эту возможность, ноrep movsb
позволяет им и должен дать ожидаемый результат. В текущих реализациях в основном влияет на загрузку, но, вероятно, не на пропускную способность большого блока. Так же,rep movsb
должен поддерживать байт-гранулярные копии, даже если вы на самом деле используете его для копирования больших блоков, кратных некоторой большой степени 2.Программное обеспечение может иметь информацию о выравнивании, размере копии и возможном псевдониме, которые не могут быть переданы аппаратному обеспечению при использовании
rep movsb
, Компиляторы часто могут определять выравнивание блоков памяти 6 и, таким образом, могут избежать значительной части работы при запуске, котораяrep movs
должен делать при каждом вызове.
Результаты теста
Вот результаты теста для множества различных методов копирования из tinymembench
на моем i7-6700HQ с частотой 2,6 ГГц (очень жаль, что у меня одинаковый процессор, поэтому мы не получаем новую точку данных...):
C copy backwards : 8284.8 MB/s (0.3%)
C copy backwards (32 byte blocks) : 8273.9 MB/s (0.4%)
C copy backwards (64 byte blocks) : 8321.9 MB/s (0.8%)
C copy : 8863.1 MB/s (0.3%)
C copy prefetched (32 bytes step) : 8900.8 MB/s (0.3%)
C copy prefetched (64 bytes step) : 8817.5 MB/s (0.5%)
C 2-pass copy : 6492.3 MB/s (0.3%)
C 2-pass copy prefetched (32 bytes step) : 6516.0 MB/s (2.4%)
C 2-pass copy prefetched (64 bytes step) : 6520.5 MB/s (1.2%)
---
standard memcpy : 12169.8 MB/s (3.4%)
standard memset : 23479.9 MB/s (4.2%)
---
MOVSB copy : 10197.7 MB/s (1.6%)
MOVSD copy : 10177.6 MB/s (1.6%)
SSE2 copy : 8973.3 MB/s (2.5%)
SSE2 nontemporal copy : 12924.0 MB/s (1.7%)
SSE2 copy prefetched (32 bytes step) : 9014.2 MB/s (2.7%)
SSE2 copy prefetched (64 bytes step) : 8964.5 MB/s (2.3%)
SSE2 nontemporal copy prefetched (32 bytes step) : 11777.2 MB/s (5.6%)
SSE2 nontemporal copy prefetched (64 bytes step) : 11826.8 MB/s (3.2%)
SSE2 2-pass copy : 7529.5 MB/s (1.8%)
SSE2 2-pass copy prefetched (32 bytes step) : 7122.5 MB/s (1.0%)
SSE2 2-pass copy prefetched (64 bytes step) : 7214.9 MB/s (1.4%)
SSE2 2-pass nontemporal copy : 4987.0 MB/s
Некоторые ключевые выводы:
-
rep movs
методы быстрее, чем все другие методы, которые не являются "невременными" 7, и значительно быстрее, чем "C", которые копируют 8 байтов за раз. - "Временные" методы быстрее, примерно на 26%, чем
rep movs
из них - но это намного меньше, чем тот, который вы сообщили (26 ГБ / с против 15 ГБ / с = ~73%). - Если вы не используете невременные хранилища, использование 8-байтовых копий из C почти так же хорошо, как 128-битная загрузка / сохранение SSE. Это связано с тем, что хороший цикл копирования может генерировать достаточное давление памяти для насыщения полосы пропускания (например, 2,6 ГГц * 1 магазин / цикл * 8 байт = 26 ГБ / с для магазинов).
- В tinymembench нет явных 256-битных алгоритмов (за исключением, вероятно, "стандартного").
memcpy
) но это, вероятно, не имеет значения из-за вышеупомянутого примечания. - Увеличенная пропускная способность невременных хранилищ приближается к временным примерно в 1,45x, что очень близко к 1,5x, которые можно ожидать, если NT исключит 1 из 3 передач (т. Е. 1 чтение, 1 запись для NT против 2 читает, 1 пишу).
rep movs
подходы лежат посередине. - Комбинация довольно низкой задержки памяти и скромной 2-канальной полосы пропускания означает, что этот конкретный чип способен насыщать пропускную способность памяти из одного потока, что резко меняет поведение.
rep movsd
кажется, использует ту же магию, что иrep movsb
на этом чипе. Это интересно, потому что ERMSB только явно нацеленmovsb
и более ранние тесты на более ранних арках с ERMSB показываютmovsb
работает намного быстрее, чемmovsd
, Это в основном академическое, так какmovsb
является более общим, чемmovsd
тем не мение.
Haswell
Глядя на результаты Haswell, любезно предоставленные iwillnotexist в комментариях, мы видим те же общие тенденции (наиболее важные результаты извлечены):
C copy : 6777.8 MB/s (0.4%)
standard memcpy : 10487.3 MB/s (0.5%)
MOVSB copy : 9393.9 MB/s (0.2%)
MOVSD copy : 9155.0 MB/s (1.6%)
SSE2 copy : 6780.5 MB/s (0.4%)
SSE2 nontemporal copy : 10688.2 MB/s (0.3%)
rep movsb
подход все еще медленнее, чем временный memcpy
, но только примерно на 14% здесь (по сравнению с ~26% в тесте Skylake). Преимущество техник NT над их временными собратьями теперь составляет ~57%, даже немного больше, чем теоретическое преимущество сокращения полосы пропускания.
Когда вы должны использовать rep movs
?
Наконец, удар по вашему актуальному вопросу: когда или почему вы должны его использовать? Это опирается на вышеизложенное и вводит несколько новых идей. К сожалению, нет простого ответа: вам придется поменять различные факторы, в том числе те, которые вы, вероятно, даже не знаете точно, например, будущие разработки.
Примечание, что альтернатива rep movsb
может быть оптимизированный libc memcpy
(включая копии, встроенные компилятором), или это может быть ручная работа memcpy
версия. Некоторые из перечисленных ниже преимуществ применимы только по сравнению с одной или другой из этих альтернатив (например, "простота" помогает против ручной версии, но не против встроенной memcpy
), но некоторые относятся к обоим.
Ограничения на доступные инструкции
В некоторых средах существует ограничение на определенные инструкции или использование определенных регистров. Например, в ядре Linux использование регистров SSE/AVX или FP, как правило, запрещено. Поэтому большинство оптимизированных memcpy
варианты не могут быть использованы, так как они зависят от регистров SSE или AVX и простого 64-битного mov
копия на основе используется на x86. Для этих платформ, используя rep movsb
позволяет большую часть производительности оптимизированного memcpy
без нарушения ограничений по SIMD-коду.
Более общим примером может быть код, который должен быть нацелен на многие поколения оборудования и который не использует аппаратную диспетчеризацию (например, использование cpuid
). Здесь вы можете быть вынуждены использовать только старые наборы инструкций, что исключает любые AVX и т. Д. rep movsb
может быть хорошим подходом, поскольку он позволяет "скрытый" доступ к более широким загрузкам и хранилищам без использования новых инструкций. Если вы ориентируетесь на аппаратное обеспечение до ERMSB, вы должны увидеть, если rep movsb
производительность там приемлемая, хотя...
Будущая проверка
Хороший аспект rep movsb
в том, что теоретически можно использовать преимущества архитектурных улучшений будущих архитектур без изменений в исходном коде. Например, когда были введены 256-битные пути данных, rep movsb
смог воспользоваться ими (как утверждает Intel) без каких-либо изменений, необходимых для программного обеспечения. Программное обеспечение, использующее 128-битные перемещения (что было оптимальным до Haswell), должно быть модифицировано и перекомпилировано.
Таким образом, это одновременно и преимущество в обслуживании программного обеспечения (не нужно менять исходный код), и преимущество для существующих двоичных файлов (не нужно развертывать новые двоичные файлы, чтобы воспользоваться преимуществами улучшения).
Насколько это важно, зависит от вашей модели обслуживания (например, от того, как часто на практике внедряются новые двоичные файлы), и от того, насколько быстро эти инструкции, вероятно, появятся в будущем, будет очень сложно. По крайней мере, Intel является своего рода руководством для использования в этом направлении, взяв на себя обязательство по крайней мере разумной производительности в будущем (15.3.3.6):
REP MOVSB и REP STOSB продолжат работать на будущих процессорах достаточно хорошо.
Перекрытие с последующей работой
Эта выгода не будет отображаться на равнине memcpy
эталонный тест, который, по определению, не должен перекрывать последующую работу, поэтому размер выгоды должен быть тщательно измерен в реальном сценарии. Использование максимального преимущества может потребовать реорганизации кода, окружающего memcpy
,
Это преимущество указано Intel в их руководстве по оптимизации (раздел 11.16.3.4) и в их словах:
Когда известно, что подсчет составляет, по меньшей мере, тысячу байт или более, использование расширенного REP MOVSB /STOSB может предоставить другое преимущество для амортизации стоимости непотребляющего кода. Эвристику можно понять, используя значение Cnt = 4096 и memset() в качестве примера:
• Реализация memset() в 256-битной SIMD должна будет выпустить / выполнить 128 экземпляров 32-байтовой операции сохранения с VMOVDQA до того, как непоследовательные последовательности команд смогут выйти на пенсию.
• Экземпляр расширенного REP STOSB с ECX= 4096 декодируется как длинный микрооперационный поток, предоставляемый аппаратными средствами, но удаляется как одна команда. Есть много операций store_data, которые должны завершиться, прежде чем результат memset() может быть использован. Поскольку завершение операции сохранения данных не связано с удалением программного порядка, значительная часть потока неиспользуемого кода может обрабатываться посредством выдачи / выполнения и удаления, по существу, без затрат, если непотребляющая последовательность не конкурирует для хранения ресурсов буфера.
Таким образом, Intel говорит, что после некоторых мопов код после rep movsb
выпустил, но в то время как много магазинов все еще в полете и rep movsb
в целом еще не удалился, мопы из следующих инструкций могут добиться большего прогресса в механизме неупорядоченности, чем они могли бы, если бы этот код пришел после цикла копирования.
Все мопы из явного цикла загрузки и хранения должны фактически удаляться отдельно в программном порядке. Это должно произойти, чтобы освободить место в ROB для следующих мопов.
Похоже, не так много подробной информации о том, как очень длинная микрокодированная инструкция rep movsb
работать точно. Мы не знаем точно, как ветви микрокода запрашивают другой поток мопов из секвенсора микрокода, или как мопы удаляются. Если отдельным мопам не нужно выходить на пенсию отдельно, возможно, вся инструкция занимает только один слот в ROB?
Когда интерфейс, который питает механизм OoO, видит rep movsb
инструкция в кэше UOP активирует ПЗУ секвенсора микрокодов (MS-ROM) для отправки мопов микрокода в очередь, которая передает этап выпуска / переименования. Вероятно, другие мопы не смогут смешаться с этим и выпустить / выполнить 8, пока rep movsb
все еще выдает, но последующие инструкции могут быть извлечены / декодированы и выпущены сразу после последнего rep movsb
UOP делает, в то время как некоторые копии еще не выполнены. Это полезно только в том случае, если хотя бы часть вашего последующего кода не зависит от результата memcpy
(что не необычно).
Теперь размер этого преимущества ограничен: самое большее, вы можете выполнить N инструкций (на самом деле) rep movsb
инструкция, в какой момент вы остановитесь, где N - размер ROB. С текущими размерами ОЗУ ~200 (192 для Haswell, 224 для Skylake) это максимальная выгода ~ 200 циклов бесплатной работы для последующего кода с IPC 1. В 200 циклов вы можете скопировать где-то около 800 байт при 10 ГБ /s, поэтому для копий такого размера вы можете получить бесплатную работу, близкую к стоимости копии (таким образом, сделав копию бесплатной).
Однако, поскольку размеры копий становятся намного больше, относительная важность этого быстро уменьшается (например, если вместо этого вы копируете 80 КБ, бесплатная работа составляет всего 1% от стоимости копирования). Тем не менее, это довольно интересно для скромных размеров копий.
Циклы копирования также не полностью блокируют выполнение последующих инструкций. Intel не вдавается в подробности о размере выгоды или о том, какие копии или окружающий код приносят наибольшую пользу. (Горячий или холодный пункт назначения или источник, код с высокой задержкой ILP или низкой задержкой ILP после).
Размер кода
Размер исполняемого кода (несколько байтов) является микроскопическим по сравнению с типичным оптимизированным memcpy
рутина. Если производительность вообще ограничена ошибками i-кеша (включая кеширование uop), уменьшенный размер кода может быть полезным.
Опять же, мы можем ограничить размер этого преимущества в зависимости от размера копии. На самом деле я не буду работать с этим численно, но интуиция заключается в том, что уменьшение размера динамического кода на B байтов может сэкономить самое большее C * B
промахов, для некоторой постоянной C. Каждый вызов memcpy
понесет стоимость (или выгоду) потери кеша один раз, но преимущество в более высокой пропускной способности при увеличении количества копируемых байтов. Таким образом, при больших передачах более высокая пропускная способность будет доминировать в эффектах кэша.
Опять же, это не то, что будет отображаться в простом тесте, где весь цикл, без сомнения, поместится в кэш UOP. Вам понадобится тест на месте, чтобы оценить этот эффект.
Оптимизация для конкретной архитектуры
Вы сообщили, что на вашем оборудовании, rep movsb
был значительно медленнее, чем платформа memcpy
, Тем не менее, даже здесь есть сообщения об обратном результате на более раннем оборудовании (например, Ivy Bridge).
Это вполне правдоподобно, поскольку кажется, что операции перемещения строк получают любовь периодически - но не у каждого поколения, поэтому вполне могут быть быстрее или, по крайней мере, связаны (в этом случае они могут выиграть, основываясь на других преимуществах) в архитектурах, где они были доведен до даты, только чтобы отставать в последующем оборудовании.
Цитирую Энди Глеу, который должен знать кое-что об этом после реализации этого на P6:
большая слабость создания быстрых строк в микрокоде заключалась в том, что [...] микрокод терял связь с каждым поколением, становясь все медленнее и медленнее, пока кто-то не успел его исправить. Так же, как в библиотеке люди теряют слух. Я предполагаю, что возможно, что одной из упущенных возможностей было использование 128-битных загрузок и хранилищ, когда они стали доступны, и так далее.
В этом случае это можно рассматривать как очередную оптимизацию, специфичную для платформы, для применения в типичном методе "каждый трюк в книге". memcpy
подпрограммы, которые вы найдете в стандартных библиотеках и JIT-компиляторах: но только для использования на архитектурах, где это лучше. Для JIT или AOT-скомпилированного материала это легко, но для статически скомпилированных двоичных файлов это требует специфической для платформы диспетчеризации, но это часто уже существует (иногда реализуется во время соединения), или mtune
Аргумент может быть использован для принятия статического решения.
Простота
Даже на Skylake, где кажется, что он отстает от самых быстрых невременных методов, он все же быстрее, чем большинство подходов, и очень прост. Это означает меньше времени на проверку, меньше загадочных ошибок, меньше времени на настройку и обновление монстра memcpy
реализация (или, наоборот, меньшая зависимость от прихотей разработчиков стандартной библиотеки, если вы полагаетесь на это).
Платформы с задержкой
Алгоритмы 9, ограничивающие пропускную способность памяти, могут фактически работать в двух основных режимах: ограничение полосы пропускания DRAM или ограничение параллелизма / задержки.
Первый режим - это тот, который вам, вероятно, знаком: подсистема DRAM имеет определенную теоретическую пропускную способность, которую вы можете довольно легко рассчитать на основе количества каналов, скорости передачи данных / ширины и частоты. Например, моя система DDR4-2133 с 2 каналами имеет максимальную пропускную способность 2,133 * 8 * 2 = 34,1 ГБ / с, как и в ARK.
Вы не будете поддерживать более высокую скорость от DRAM (и, как правило, несколько меньше из-за различной неэффективности), добавляемого ко всем ядрам сокета (т. Е. Это глобальный предел для систем с одним сокетом).
Другое ограничение связано с тем, сколько одновременных запросов ядро может выдать подсистеме памяти. Представьте, что ядро может одновременно выполнять только один запрос для 64-байтовой строки кэша - когда запрос завершен, вы можете выдать другой. Предположим также очень быструю задержку памяти 50 нс. Тогда, несмотря на большую пропускную способность DRAM (34,1 ГБ / с), вы получите 64 байта / 50 нс = 1,28 ГБ / с или менее 4% от максимальной пропускной способности.
На практике ядра могут выдавать более одного запроса за раз, но не неограниченное количество. Обычно подразумевается, что между L1 и остальной частью иерархии памяти имеется только 10 буферов заполнения строки на ядро и, возможно, 16 или около того буферов заполнения между L2 и DRAM. Предварительная выборка конкурирует за те же ресурсы, но, по крайней мере, помогает уменьшить эффективную задержку. Для получения более подробной информации посмотрите на любые замечательные сообщения , написанные Dr. Bandwidth на эту тему, в основном на форумах Intel.
Тем не менее, большинство последних процессоров ограничены этим фактором, а не пропускной способностью ОЗУ. Обычно они достигают 12–20 ГБ / с на ядро, а пропускная способность ОЗУ может составлять 50+ ГБ / с (в 4-канальной системе). Лишь некоторые недавние 2-канальные "клиентские" ядра поколения, которые, кажется, имеют лучшие неядерные ядра, возможно, большее количество линейных буферов могут достичь предела DRAM на одном ядре, и наши чипы Skylake, похоже, являются одним из них.
Теперь, конечно, есть причина, по которой Intel разрабатывает системы с пропускной способностью DRAM 50 ГБ / с, в то же время поддерживая пропускную способность < 20 ГБ / с на ядро из-за ограничений параллелизма: первое ограничение распространяется на сокеты, а второе - на ядро. Таким образом, каждое ядро в 8-ядерной системе может выдавать запросы на 20 ГБ / с, после чего они снова будут ограничены DRAM.
Почему я продолжаю об этом? Потому что лучший memcpy
реализация часто зависит от того, в каком режиме вы работаете. Как только вы ограничены в DRAM BW (как, видимо, и есть наши чипы, но большинство не на одном ядре), использование невременных операций записи становится очень важным, так как экономит время чтения владение, которое обычно тратит 1/3 вашей пропускной способности. Вы видите это точно в результатах теста выше: реализации memcpy, которые не используют хранилища NT, теряют 1/3 своей пропускной способности.
Однако если у вас ограничен параллелизм, ситуация выравнивается, а иногда и наоборот. У вас есть запасная пропускная способность DRAM, поэтому хранилища NT не помогают, и они могут даже повредить, поскольку они могут увеличить задержку, поскольку время передачи обслуживания для буфера строки может быть больше, чем в сценарии, когда предварительная выборка приводит линию RFO в LLC (или даже L2), а затем магазин завершает свою работу в LLC для эффективной более низкой задержки. И, наконец, на основных ядрах серверов обычно гораздо медленнее хранилища NT, чем на клиентских (и с высокой пропускной способностью), что усиливает этот эффект.
Так что на других платформах вы можете обнаружить, что NT-хранилища менее полезны (по крайней мере, если вы заботитесь об однопоточной производительности) и, возможно, rep movsb
где выигрывает (если он получает лучшее из обоих миров).
Действительно, этот последний пункт является вызовом для большинства испытаний. Я знаю, что хранилища NT теряют свое очевидное преимущество для однопоточных тестов на большинстве архитектур (включая текущие серверные архивы), но я не знаю, как rep movsb
будет выполнять относительно...
Рекомендации
Другие хорошие источники информации, не интегрированные в выше.
Comp.Arch расследование rep movsb
против альтернатив. Множество хороших заметок о прогнозировании ветвлений и реализации подхода, который я часто предлагал для небольших блоков: использование перекрывающихся первых и / или последних операций чтения / записи вместо попыток записи только точно необходимого количества байтов (например, реализация все копии размером от 9 до 16 байтов в виде двух 8-байтовых копий, которые могут перекрываться до 7 байтов).
1 Предположительно, намерение состоит в том, чтобы ограничить его случаями, когда, например, размер кода очень важен.
2 См. Раздел 3.7.5: Префикс REP и перемещение данных.
3 Важно отметить, что это применимо только к различным хранилищам в пределах одной инструкции: после завершения блок хранилищ все еще выглядит упорядоченным по отношению к предыдущим и последующим хранилищам. Таким образом, код может видеть магазины из rep movs
вышли из строя по отношению друг к другу, но не по отношению к предыдущим или последующим магазинам (и это последняя гарантия, которая вам обычно требуется). Это будет проблемой только в том случае, если вы используете конечный пункт назначения копирования в качестве флага синхронизации вместо отдельного хранилища.
4 Обратите внимание, что не временные дискретные магазины также избегают большинства требований заказа, хотя на практике rep movs
имеет еще большую свободу, поскольку в магазинах WC/NT все еще существуют некоторые ограничения на порядок.
5 Это было распространено в последней части 32-битной эры, когда многие чипы имели 64-битные тракты данных (например, для поддержки FPU, которые имели поддержку 64-битных double
тип). Сегодня "стерилизованные" чипы, такие как марки Pentium или Celeron, отключили AVX, но предположительно rep movs
Микрокод все еще может использовать 256b загрузок / хранилищ.
6 Например, из-за правил выравнивания языка, атрибутов или операторов выравнивания, правил совмещения имен или другой информации, определенной во время компиляции. В случае выравнивания, даже если точное выравнивание не может быть определено, они, по крайней мере, смогут поднять проверки выравнивания из циклов или иным образом исключить избыточные проверки.
7 Я делаю предположение, что "стандарт" memcpy
выбирает невременный подход, который весьма вероятен для этого размера буфера.
8 Это не обязательно очевидно, так как это может быть случай, когда поток UOP, генерируемый rep movsb
просто монополизирует отправку, и тогда это будет очень похоже на явное mov
дело. Однако, похоже, что это не так - мопы из последующих инструкций могут смешиваться с мопами из микрокодера rep movsb
,
То есть те, которые могут выдавать большое количество независимых запросов памяти и, следовательно, насыщать доступную полосу пропускания DRAM-к-ядру, из которых memcpy
будет дочерним объектом плаката (и применительно к нагрузкам с чисто латентной привязкой, таким как отслеживание указателя).
Улучшенный REP MOVSB (Ivy Bridge и позже)
Микроархитектура Ivy Bridge (процессоры, выпущенные в 2012 и 2013 годах) представила Enhanced REP MOVSB (нам еще нужно проверить соответствующий бит) и позволила нам быстро копировать память.
Самые дешевые версии более поздних процессоров - Kaby Lake Celeron и Pentium, выпущенные в 2017 году, не имеют AVX, который можно было бы использовать для быстрого копирования памяти, но все еще имеют Enhanced REP MOVSB.
REP MOVSB (ERMSB) работает быстрее, чем AVX-копия или регистр общего назначения, если размер блока составляет не менее 256 байт. Для блоков ниже 64 байт это НАМНОГО медленнее, потому что в ERMSB высокий внутренний запуск - около 35 циклов.
См. Руководство Intel по оптимизации, раздел 3.7.6 Расширенные операции REP MOVSB и STOSB (ERMSB) http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf
- стартовая стоимость 35 циклов;
- адреса источника и назначения должны быть выровнены по 16-байтовой границе;
- исходный регион не должен перекрываться с регионом назначения;
- длина должна быть кратна 64, чтобы обеспечить более высокую производительность;
- направление должно быть вперед (CLD).
Как я уже говорил ранее, REP MOVSB начинает превосходить другие методы, когда длина составляет не менее 256 байт, но чтобы увидеть явное преимущество по сравнению с копией AVX, длина должна быть больше 2048 байт.
На эффект выравнивания, если REP MOVSB против AVX копируют, Руководство Intel дает следующую информацию:
- если исходный буфер не выровнен, влияние на реализацию ERMSB по сравнению с 128-битным AVX будет аналогичным;
- если буфер назначения не выровнен, влияние на реализацию ERMSB может быть ухудшено на 25%, в то время как реализация memcpy в 128-битной AVX может ухудшить только 5% по сравнению с 16-байтовым выровненным сценарием.
Я провел тесты на Intel Core i5-6600 под 64-битной версией и сравнил REP MOVSB memcpy() с простым MOV RAX, [SRC]; MOV [DST], реализация RAX, когда данные помещаются в кэш L1:
REP MOVSB memcpy():
- 1622400000 data blocks of 32 bytes took 17.9337 seconds to copy; 2760.8205 MB/s
- 1622400000 data blocks of 64 bytes took 17.8364 seconds to copy; 5551.7463 MB/s
- 811200000 data blocks of 128 bytes took 10.8098 seconds to copy; 9160.5659 MB/s
- 405600000 data blocks of 256 bytes took 5.8616 seconds to copy; 16893.5527 MB/s
- 202800000 data blocks of 512 bytes took 3.9315 seconds to copy; 25187.2976 MB/s
- 101400000 data blocks of 1024 bytes took 2.1648 seconds to copy; 45743.4214 MB/s
- 50700000 data blocks of 2048 bytes took 1.5301 seconds to copy; 64717.0642 MB/s
- 25350000 data blocks of 4096 bytes took 1.3346 seconds to copy; 74198.4030 MB/s
- 12675000 data blocks of 8192 bytes took 1.1069 seconds to copy; 89456.2119 MB/s
- 6337500 data blocks of 16384 bytes took 1.1120 seconds to copy; 89053.2094 MB/s
MOV RAX... memcpy():
- 1622400000 data blocks of 32 bytes took 7.3536 seconds to copy; 6733.0256 MB/s
- 1622400000 data blocks of 64 bytes took 10.7727 seconds to copy; 9192.1090 MB/s
- 811200000 data blocks of 128 bytes took 8.9408 seconds to copy; 11075.4480 MB/s
- 405600000 data blocks of 256 bytes took 8.4956 seconds to copy; 11655.8805 MB/s
- 202800000 data blocks of 512 bytes took 9.1032 seconds to copy; 10877.8248 MB/s
- 101400000 data blocks of 1024 bytes took 8.2539 seconds to copy; 11997.1185 MB/s
- 50700000 data blocks of 2048 bytes took 7.7909 seconds to copy; 12710.1252 MB/s
- 25350000 data blocks of 4096 bytes took 7.5992 seconds to copy; 13030.7062 MB/s
- 12675000 data blocks of 8192 bytes took 7.4679 seconds to copy; 13259.9384 MB/s
Таким образом, даже на 128-битных блоках REP MOVSB медленнее, чем простая копия MOV RAX в цикле (не развернутая). Реализация ERMSB начинает превосходить цикл MOV RAX только начиная с 256-байтовых блоков.
Обычный (не улучшенный) REP MOVS на Nehalem и позже
Удивительно, но в предыдущих архитектурах (Nehalem и более поздних), которые еще не имели Enhanced REP MOVB, была довольно быстрая реализация REP MOVSD/MOVSQ (но не REP MOVSB /MOVSW) для больших блоков, но недостаточно большая, чтобы увеличить кэш L1.
В Руководстве по оптимизации Intel (2.5.6 REP String Enhancement) приводится следующая информация, относящаяся к микроархитектуре Nehalem - процессорам Intel Core i5, i7 и Xeon, выпущенным в 2009 и 2010 годах.
РЭП МОВСБ
Задержка для MOVSB составляет 9 циклов, если ECX < 4; в противном случае REP MOVSB с ECX > 9 будет стоить стартовый цикл в 50 циклов.
- крошечная строка (ECX < 4): задержка REP MOVSB составляет 9 циклов;
- маленькая строка (ECX от 4 до 9): в руководстве Intel нет официальной информации, вероятно, более 9 циклов, но менее 50 циклов;
- длинная строка (ECX > 9): начальная стоимость 50 циклов.
Мой вывод: REP MOVSB практически бесполезен для Nehalem.
MOVSW / MOVSD / MOVSQ
Цитата из Руководства по оптимизации Intel (2.5.6 REP String Enhancement):
- Короткая строка (ECX <= 12): задержка REP MOVSW/MOVSD/MOVSQ составляет около 20 циклов.
- Быстрая строка (ECX >= 76: исключая REP MOVSB): реализация процессора обеспечивает аппаратную оптимизацию, перемещая как можно больше фрагментов данных в 16 байтов. Задержка строки REP будет изменяться, если один из 16-байтовых переносов данных пересекает границу строки кэша: = без разделения: задержка состоит из стоимости запуска около 40 циклов, и каждые 64 байта данных добавляют 4 цикла. = Разделение кэша: задержка состоит из начальной стоимости около 35 циклов, и каждые 64 байта данных добавляют 6 циклов.
- Промежуточные длины строк: задержка REP MOVSW/MOVSD/MOVSQ имеет начальную стоимость около 15 циклов плюс один цикл для каждой итерации перемещения данных в word/dword/qword.
Кажется, Intel здесь не прав. Из приведенной выше цитаты мы понимаем, что для очень больших блоков памяти REP MOVSW работает так же быстро, как REP MOVSD/MOVSQ, но тесты показали, что только REP MOVSD / MOVSQ быстр, а REP MOVSW даже медленнее, чем REP MOVSB в Nehalem и Westmere.,
Согласно информации, предоставленной Intel в руководстве, на предыдущих микроархитектурах Intel (до 2008 года) затраты на запуск еще выше.
Вывод: если вам просто нужно скопировать данные, которые соответствуют кэш-памяти L1, просто 4 цикла для копирования 64 байтов данных - это отлично, и вам не нужно использовать регистры XMM!
REP MOVSD / MOVSQ - это универсальное решение, которое отлично работает на всех процессорах Intel (не требуется ERMSB), если данные соответствуют кэш-памяти L1.
Вот тесты REP MOVS*, когда источник и назначение находились в кэше L1, блоков, достаточно больших, чтобы не подвергаться серьезному влиянию затрат на запуск, но не настолько больших, чтобы превышать размер кэша L1. Источник: http://users.atw.hu/instlatx64/
Йона (2006-2008)
REP MOVSB 10.91 B/c
REP MOVSW 10.85 B/c
REP MOVSD 11.05 B/c
Нехалем (2009-2010)
REP MOVSB 25.32 B/c
REP MOVSW 19.72 B/c
REP MOVSD 27.56 B/c
REP MOVSQ 27.54 B/c
Вестмер (2010-2011)
REP MOVSB 21.14 B/c
REP MOVSW 19.11 B/c
REP MOVSD 24.27 B/c
Ivy Bridge (2012-2013) - с улучшенным REP MOVSB
REP MOVSB 28.72 B/c
REP MOVSW 19.40 B/c
REP MOVSD 27.96 B/c
REP MOVSQ 27.89 B/c
SkyLake (2015-2016) - с расширенным REP MOVSB
REP MOVSB 57.59 B/c
REP MOVSW 58.20 B/c
REP MOVSD 58.10 B/c
REP MOVSQ 57.59 B/c
Озеро Кабы (2016-2017) - с расширенным РЭП МОВСБ
REP MOVSB 58.00 B/c
REP MOVSW 57.69 B/c
REP MOVSD 58.00 B/c
REP MOVSQ 57.89 B/c
Как видите, реализация REP MOVS существенно отличается от одной микроархитектуры к другой. На некоторых процессорах, таких как Ivy Bridge - REP MOVSB работает быстрее, хотя и немного быстрее, чем REP MOVSD/MOVSQ, но не сомневаюсь, что на всех процессорах, начиная с Nehalem, REP MOVSD/MOVSQ работает очень хорошо - вам даже не нужно "Enhanced REP MOVSB ", поскольку на Ivy Bridge (2013) с Enhacnced REP MOVSB, REP MOVSD показывает тот же байт на тактовые данные, что и на Nehalem (2010) без Enhacnced REP MOVSB, тогда как на самом деле REP MOVSB стал очень быстрым только после SkyLake (2015) - в два раза быстрее, чем на Ivy Bridge. Таким образом, этот расширенный бит REP MOVSB в CPUID может сбивать с толку - он только показывает, что REP MOVSB
само по себе это нормально, но не то, что любой REP MOVS*
быстрее.
Наиболее запутанная реализация ERMBSB - это микроархитектура Ivy Bridge. Да, на очень старых процессорах до ERMSB REP MOVS* для больших блоков использовал функцию протокола кэширования, которая недоступна для обычного кода (без RFO). Но этот протокол больше не используется на Ivy Bridge с ERMSB. Согласно комментариям Энди Глеу на ответ "почему сложная система memcpy/memset лучше?" Судя по ответу Питера Кордеса, функция протокола кэширования, которая недоступна обычному коду, когда-то использовалась на старых процессорах, но больше не использовалась в Ivy Bridge. И приходит объяснение того, почему затраты на запуск настолько высоки для REP MOVS*: "Большие накладные расходы на выбор и настройку правильного метода в основном связаны с отсутствием предсказания ветвления микрокода". Также было интересно отметить, что Pentium Pro (P6) в 1996 году внедрил REP MOVS* с 64-битной загрузкой и хранением микрокодов и протоколом кэширования без RFO - они не нарушали упорядочение памяти, в отличие от ERMSB в Ivy Bridge.
отказ
- Этот ответ актуален только для случаев, когда данные источника и назначения соответствуют кэш-памяти L1. В зависимости от обстоятельств следует учитывать особенности доступа к памяти (кэш и т. Д.). Prefetch и NTI могут давать лучшие результаты в определенных случаях, особенно на процессорах, которые еще не имели Enhanced REP MOVSB. Даже на этих старых процессорах REP MOVSD мог использовать функцию протокола кэширования, недоступную обычному коду.
- Информация в этом ответе относится только к процессорам Intel, а не к процессорам других производителей, таких как AMD, которые могут иметь лучшие или худшие реализации инструкций REP MOVS*.
- Я представил результаты тестов для SkyLake и Kaby Lake только для подтверждения - эти архитектуры имеют одинаковые данные о цикле на инструкцию.
- Все названия продуктов, торговые марки и зарегистрированные торговые марки являются собственностью их соответствующих владельцев.
Это не ответ на поставленный вопрос (а), только мои результаты (и личные выводы) при попытке выяснить.
В итоге: GCC уже оптимизирует memset()
/ memmove()
/ memcpy()
(см., например, gcc/config/i386/i386.c:expand_set_or_movmem_via_rep() в источниках GCC; также ищите stringop_algs
в том же файле, чтобы увидеть архитектурно-зависимые варианты). Таким образом, нет никаких оснований ожидать значительных выгод от использования собственного варианта с GCC (если вы не забыли важные вещи, такие как атрибуты выравнивания для выровненных данных, или не включили достаточно специфические оптимизации, такие как -O2 -march= -mtune=
). Если вы согласны, то ответы на поставленный вопрос более или менее неактуальны на практике.
(Я только хотел, чтобы был memrepeat()
, противоположно memcpy()
по сравнению с memmove()
, это будет повторять начальную часть буфера, чтобы заполнить весь буфер.)
В настоящее время у меня используется машина Ivy Bridge (ноутбук Core i5-6200U, ядро Linux 4.4.0 x86-64, с erms
в /proc/cpuinfo
флаги). Потому что я хотел выяснить, могу ли я найти случай, когда пользовательский вариант memcpy() основан на rep movsb
превзойдет прямолинейное memcpy()
Я написал слишком сложный тест.
Основная идея заключается в том, что основная программа выделяет три большие области памяти: original
, current
, а также correct
, каждый точно такой же размер, и, по крайней мере, выровненный по странице. Операции копирования сгруппированы в наборы, причем каждый набор имеет разные свойства, например, все источники и цели выровнены (по некоторому количеству байтов) или все длины находятся в одном диапазоне. Каждый набор описывается с использованием массива src
, dst
, n
триплеты, где все src
в src+n-1
а также dst
в dst+n-1
полностью в пределах current
площадь.
Xorshift * PRNG используется для инициализации original
к случайным данным. (Как я уже предупреждал выше, это слишком сложно, но я хотел убедиться, что я не оставляю легкие ярлыки для компилятора.) correct
Площадь получается, начиная с original
данные в current
, применяя все тройки в текущем наборе, используя memcpy()
предоставленный библиотекой C, и копирование current
площадь для correct
, Это позволяет проверять правильность поведения каждой тестируемой функции.
Каждый набор операций копирования рассчитан на большое количество раз с использованием одной и той же функции, и их медиана используется для сравнения. (На мой взгляд, медиана имеет наибольшее значение в бенчмаркинге и обеспечивает разумную семантику - функция, по крайней мере, настолько быстра, по крайней мере, наполовину.)
Чтобы избежать оптимизации компилятора, у меня есть программа, загружающая функции и тесты динамически, во время выполнения. Все функции имеют одинаковую форму, void function(void *, const void *, size_t)
- обратите внимание, что в отличие от memcpy()
а также memmove()
они ничего не возвращают. Тесты (именованные наборы операций копирования) генерируются динамически с помощью вызова функции (который принимает указатель на current
площадь и ее размер в качестве параметров, среди прочих).
К сожалению, я еще не нашел ни одного набора, где
static void rep_movsb(void *dst, const void *src, size_t n)
{
__asm__ __volatile__ ( "rep movsb\n\t"
: "+D" (dst), "+S" (src), "+c" (n)
:
: "memory" );
}
будет бить
static void normal_memcpy(void *dst, const void *src, size_t n)
{
memcpy(dst, src, n);
}
с помощью gcc -Wall -O2 -march=ivybridge -mtune=ivybridge
использование GCC 5.4.0 на вышеупомянутом ноутбуке Core i5-6200U с 64-битным ядром linux-4.4.0. Однако копирование выровненных по размеру фрагментов размером 4096 байт близко.
Это означает, что, по крайней мере, до сих пор я не нашел случая использования rep movsb
вариант memcpy будет иметь смысл. Это не значит, что такого случая нет; Я просто не нашел ни одного.
(На данный момент код - беспорядок спагетти, который мне больше стыдно, чем горжусь, поэтому я опущу публикацию источников, если кто-то не спросит. Хотя приведенного выше описания должно быть достаточно, чтобы написать лучшее).
Это меня не сильно удивляет. Компилятор C может вывести много информации о выравнивании указателей операнда и о том, является ли число копируемых байтов константой времени компиляции, кратным подходящей степени двух. Эта информация может и будет / должна использоваться компилятором для замены библиотеки C memcpy()
/ memmove()
функционирует со своим.
GCC делает именно это (см., Например, gcc/config/i386/i386.c:expand_set_or_movmem_via_rep() в источниках GCC; также ищите stringop_algs
в том же файле, чтобы увидеть архитектурно-зависимые варианты). В самом деле, memcpy()
/ memset()
/ memmove()
уже был отдельно оптимизирован для нескольких вариантов процессоров x86; было бы весьма удивительно, если бы разработчики GCC еще не включили поддержку erms.
GCC предоставляет несколько атрибутов функций, которые разработчики могут использовать для обеспечения хорошего сгенерированного кода. Например, alloc_align (n)
сообщает GCC, что функция возвращает память по крайней мере n
байт. Приложение или библиотека могут выбрать, какую реализацию функции использовать во время выполнения, создав "функцию распознавателя" (которая возвращает указатель на функцию) и определив функцию, используя ifunc (resolver)
приписывать.
Одним из наиболее распространенных шаблонов, которые я использую в своем коде для этого, является
some_type *pointer = __builtin_assume_aligned(ptr, alignment);
где ptr
какой-то указатель, alignment
количество байтов, к которому оно выровнено; GCC тогда знает / предполагает, что pointer
выровнен к alignment
байт.
Еще одна полезная встроенная функция, хотя ее гораздо сложнее правильно использовать, __builtin_prefetch()
, Чтобы максимизировать общую пропускную способность / эффективность, я обнаружил, что минимизация задержек в каждой подоперации дает наилучшие результаты. (Для копирования разбросанных элементов в последовательное временное хранилище это сложно, так как предварительная выборка обычно включает в себя полную строку кэша; если слишком много элементов предварительно выбрано, большая часть кэша тратится впустую из-за хранения неиспользуемых элементов.)
Вы говорите, что хотите:
ответ, который показывает, когда ERMSB полезен
Но я не уверен, что это означает, что вы думаете, что это значит. Глядя на документы 3.7.6.1, на которые вы ссылаетесь, он явно говорит:
реализация memcpy с использованием ERMSB может не достичь того же уровня пропускной способности, что и при использовании альтернатив 256-битной или 128-битной AVX, в зависимости от длины и коэффициентов выравнивания.
Так просто потому что CPUID
указывает на поддержку ERMSB, что не является гарантией того, что REP MOVSB будет самым быстрым способом копирования памяти. Это просто означает, что он не будет плохим, как в некоторых предыдущих процессорах.
Однако то, что существуют альтернативы, которые при определенных условиях могут работать быстрее, не означает, что REP MOVSB бесполезен. Теперь, когда штрафы за производительность, которые несла эта инструкция, исчезли, это снова потенциально полезная инструкция.
Помните, это небольшой кусочек кода (2 байта!) По сравнению с некоторыми из более сложных процедур memcpy, которые я видел. Поскольку загрузка и запуск больших кусков кода также имеет штраф (удаление некоторого другого кода из кэша процессора), иногда "преимущество" AVX и др. Будет компенсировано влиянием, которое оно оказывает на остальную часть вашего кода. код. Зависит от того, что вы делаете.
Вы также спрашиваете:
Почему пропускная способность намного ниже с REP MOVSB? Что я могу сделать, чтобы улучшить это?
Невозможно "что-то сделать", чтобы REP MOVSB работал быстрее. Он делает то, что делает.
Если вам нужны более высокие скорости, которые вы видите из memcpy, вы можете найти источник для этого. Это где-то там. Или вы можете отследить это в отладчике и увидеть фактические пути к коду. Я ожидаю, что он использует некоторые из этих инструкций AVX для работы со 128 или 256 битами одновременно.
Или вы можете просто... Ну, вы просили нас не говорить этого.
Есть гораздо более эффективные способы перемещения данных. В наши дни реализация memcpy
будет генерировать специфичный для архитектуры код из компилятора, который оптимизирован на основе выравнивания памяти данных и других факторов. Это позволяет лучше использовать не временные инструкции кеша, а также XMM и другие регистры в мире x86.
Когда вы жестко код rep movsb
предотвращает это использование встроенных функций.
Поэтому для чего-то вроде memcpy
, если вы не пишете что-то, что будет привязано к очень специфической части оборудования, и если вы не собираетесь тратить время на написание высокооптимизированного memcpy
функция в сборке (или использование встроенных функций уровня C), вам гораздо лучше позволить компилятору выяснить это для вас.
В целом memcpy()
руководство:
a) Если копируемые данные имеют крошечный размер (менее 20 байт) и имеют фиксированный размер, пусть это сделает компилятор. Причина: компилятор может использовать обычный mov
инструкции и избежать накладных расходов при запуске.
б) Если копируемые данные невелики (менее 4 КиБ) и гарантированно выровнены, используйте rep movsb
(если ERMSB поддерживается) или rep movsd
(если ERMSB не поддерживается). Причина. Использование альтернатив SSE или AVX приводит к огромным "накладным расходам при запуске", прежде чем что-либо копировать.
c) Если копируемые данные невелики (менее 4 КиБ) и их выравнивание не гарантируется, используйте rep movsb
, Причина: использование SSE или AVX или использование rep movsd
для большей части этого плюс некоторые rep movsb
в начале или в конце, слишком много накладных расходов.
г) Для всех остальных случаев используйте что-то вроде этого:
mov edx,0
.again:
pushad
.nextByte:
pushad
popad
mov al,[esi]
pushad
popad
mov [edi],al
pushad
popad
inc esi
pushad
popad
inc edi
pushad
popad
loop .nextByte
popad
inc edx
cmp edx,1000
jb .again
Причина: это будет настолько медленным, что заставит программистов находить альтернативу, не связанную с копированием огромных массивов данных; и получающееся в результате программное обеспечение будет значительно быстрее, потому что было исключено копирование больших объемов данных.