Улучшенный REP MOVSB ​​для memcpy

Я хотел бы использовать расширенный REP MOVSB ​​(ERMSB), чтобы получить высокую пропускную способность для пользовательского memcpy.

ERMSB была представлена ​​с микроархитектурой Ivy Bridge. В разделе Руководство по оптимизации Intel, если вы не знаете, что такое ERMSB.

Единственный способ, которым я это знаю, - это встроенная сборка. Я получил следующую функцию из https://groups.google.com/forum/#!topic/gnu.gcc.help/-Bmlm_EG_fE

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 при потоковой передаче в память на Айвибридже и Хасуэлле (но имейте в виду, что pre-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 CPU по-прежнему работает до частоты простоя 0.8 GHz. Только с performance без SpeedStep процессор работает с постоянной частотой.

Я использовал, например, sudo cpufreq-set -r performance (потому что cpufreq-set давал странные результаты), чтобы изменить настройки мощности. Это снова включилось, поэтому мне пришлось отключить турбо.

Ответы

Ответ 1

Эта тема очень близка моему сердцу и недавним исследованиям, поэтому я буду смотреть на нее с нескольких сторон: история, некоторые технические заметки (в основном академические), результаты тестов на моей коробке и, наконец, попытка ответить на ваш актуальный вопрос когда и где 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 если вы знаете, что размер равен трем).

Они продолжают говорить:

Строковые инструкции MOVE/STORE имеют несколько гранулярностей данных. Для эффективного перемещения данных предпочтительнее большая степень детализации данных. Это означает, что лучшая эффективность может быть достигнута путем разложения произвольного значения счетчика на количество двойных слов плюс однобайтовые перемещения со значением счетчика, меньшим или равным 3.

Это, конечно, кажется неправильным на современном оборудовании с ERMSB, где rep movsb по крайней мере так же быстро или быстрее, чем варианты movd или movq для больших копий.

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

Затем они подробно освещают ERMSB в разделе 3.7.6.

Я не буду подробно останавливаться на оставшемся совете, но суммирую хорошие моменты в разделе "зачем его использовать" ниже.

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

Технические соображения

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

Преимущества для rep movs

  1. Когда выдается команда rep movs, CPU знает, что должен быть передан весь блок известного размера. Это может помочь ему оптимизировать работу так, как это невозможно с дискретными инструкциями, например:

    • Избегание запроса RFO, когда он знает, что вся строка кэша будет перезаписана.
    • Выдача запросов на предварительную выборку немедленно и точно. Аппаратная предварительная выборка хорошо справляется с обнаружением шаблонов memcpy -like, но для запуска все равно требуется несколько операций чтения, и она будет "перегружаться" перед многими строками кэша за пределами конца скопированной области. rep movsb точно знает размер региона и может точно rep movsb.
  2. По-видимому, нет никакой гарантии упорядочения между магазинами в течение 3-хrep movs которые могут помочь упростить согласованный трафик и просто другие аспекты перемещения блока, по сравнению с простыми инструкциями mov которые должны подчиняться довольно строгому упорядочению памяти 4.

  3. В принципе, команда rep movs может использовать различные архитектурные приемы, которые не представлены в ISA. Например, в архитектурах могут быть более широкие внутренние пути данных, которые ISA предоставляет 5, и rep movs могут использовать их внутри.

Недостатки

  1. rep movsb должен реализовывать определенную семантику, которая может быть сильнее, чем основное программное обеспечение. В частности, memcpy запрещает перекрывающиеся области и поэтому может игнорировать эту возможность, но rep movsb позволяет им и должен давать ожидаемый результат. В текущих реализациях в основном влияет на загрузку, но, вероятно, не на пропускную способность большого блока. Аналогично, rep movsb должен поддерживать байт-гранулярные копии, даже если вы на самом деле используете его для копирования больших блоков, кратных некоторой большой степени 2.

  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 быстрее, чем все другие методы, которые не являются " 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 к rep movs лежат посередине.
  • Комбинация довольно низкой задержки памяти и скромной 2-канальной полосы пропускания означает, что этот конкретный чип способен насыщать пропускную способность памяти из одного потока, что резко меняет поведение.
  • rep movsd похоже, использует ту же магию, что и rep movsb на этом чипе. Это интересно, потому что ERMSB только явно нацелен на movsb а более ранние тесты на более ранних архивах с ERMSB показывают, что movsb работает намного быстрее чем movsd. В основном это академический 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 все еще медленнее, чем 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 -based используется на x86. Для этих платформ использование rep movsb обеспечивает большую часть производительности оптимизированного memcpy без нарушения ограничений на код SIMD.

Более общим примером может быть код, предназначенный для многих поколений оборудования и не использующий аппаратную диспетчеризацию (например, с использованием cpuid). Здесь вы можете быть вынуждены использовать только более старые наборы инструкций, что исключает любые AVX и т.д. rep movsb может быть хорошим подходом, поскольку он позволяет "скрытый" доступ к более широким загрузкам и хранилищам без использования новых инструкций. Если вы ориентируетесь на аппаратное обеспечение, предшествующее ERMSB, вам придется посмотреть, приемлема ли там производительность rep movsb, хотя...

Будущая проверка

rep movsb аспектом 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, но, хотя многие магазины все еще находятся в полете, а rep movsb в целом еще не удалился, мопы из следующих инструкций могут добиться большего прогресса через машин, чем они могли бы, если бы этот код появился после цикла копирования.

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

Похоже, не так много подробной информации о том, как работают очень длинные микрокодированные инструкции, такие как rep movsb. Мы не знаем точно, как ветки микрокода запрашивают другой поток мопов из секвенсора микрокода, или как мопы удаляются. Если отдельным мопам не нужно выходить на пенсию отдельно, возможно, вся инструкция занимает только один слот в ROB?

Когда внешний интерфейс, который передает механизм OoO, видит rep movsb в кэше rep movsb, он активирует ПЗУ rep movsb микрокодов (MS-ROM) для отправки микрокодов в очередь, которая передает этап выпуска/переименования. Возможно, что другие rep movsb смешаться с этим и выпустить/выполнить 8, пока rep movsb все еще выдает, но последующие инструкции могут быть извлечены/декодированы и rep movsb сразу после того, как последний rep movsb делает, в то время как некоторые из копий не имеют еще не выполнено. Это полезно только в том случае, если хотя бы часть вашего последующего кода не зависит от результата memcpy (что не является необычным).

Теперь размер этого преимущества ограничен: самое большее, вы можете выполнить N инструкций (в действительности, rep movsb) за пределами медленной команды rep movsb, в которой вы остановитесь, где N - размер ROB. При текущих размерах 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 за собой memcpy кеш-памяти (или выгода), но преимущество более высокой пропускной способности зависит от количества копируемых байтов. Таким образом, при больших передачах более высокая пропускная способность будет доминировать в эффектах кэша.

Опять же, это не то, что будет отображаться в простом тесте, где весь цикл, без сомнения, поместится в кэш UOP. Вам понадобится тест на месте, чтобы оценить этот эффект.

Оптимизация для конкретной архитектуры

Вы сообщили, что на вашем оборудовании rep movsb был значительно медленнее, чем платформа memcpy. Тем не менее, даже здесь есть сообщения об обратном результате на более раннем оборудовании (например, Ivy Bridge).

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

Цитирование Энди Glew, который должен знать вещь или два об этом после реализации их на Р6:

большая слабость создания быстрых строк в микрокоде заключалась в том, что [...] микрокод терял связь с каждым поколением, становясь все медленнее и медленнее, пока кто-то не успел его исправить. Так же, как в библиотеке люди теряют слух. Я предполагаю, что возможно, что одной из упущенных возможностей было использование 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 микрокоды могут по-прежнему использовать 256-битные загрузки/хранилища.

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

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

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

9 Т.е. те, которые могут выдавать большое количество независимых запросов памяти и, следовательно, насыщать доступную полосу пропускания DRAM-к-ядру, из которых memcpy будет дочерним элементом плаката (и в сочетании с нагрузками с чисто латентной привязкой, такими как погоня за указателями).

Ответ 2

Улучшенный 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-архитектуры оптимизация-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.

отказ

  1. Этот ответ актуален только для случаев, когда данные источника и назначения соответствуют кэш-памяти L1. В зависимости от обстоятельств следует учитывать особенности доступа к памяти (кэш и т.д.). Prefetch и NTI могут давать лучшие результаты в определенных случаях, особенно на процессорах, которые еще не имели Enhanced REP MOVSB. Даже на этих старых процессорах REP MOVSD мог использовать функцию протокола кэширования, недоступную обычному коду.
  2. Информация в этом ответе относится только к процессорам Intel, а не к процессорам других производителей, таких как AMD, которые могут иметь лучшие или худшие реализации инструкций REP MOVS *.
  3. Я представил результаты тестов для SkyLake и Kaby Lake только для подтверждения - эти архитектуры имеют одинаковые данные о цикле на инструкцию.
  4. Все названия продуктов, торговые марки и зарегистрированные торговые марки являются собственностью их соответствующих владельцев.

Ответ 3

Вы говорите, что хотите:

ответ, который показывает, когда 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 бит за раз.

Или вы можете просто... Ну, вы попросили нас не говорить этого.

Ответ 4

Это не ответ на поставленный вопрос (вопросы), только мои результаты (и личные выводы) при попытке выяснить.

В итоге: 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.

A 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(). Чтобы максимизировать общую пропускную способность/эффективность, я обнаружил, что минимизация задержек в каждой подоперации дает наилучшие результаты. (Для копирования разбросанных элементов в последовательное временное хранилище это сложно, так как предварительная выборка обычно включает в себя полную строку кэша, если слишком много элементов предварительно загружено, большая часть кеша теряется, сохраняя неиспользуемые элементы.)

Ответ 5

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

При использовании жесткого кода rep movsb это предотвращает использование встроенных функций.

Следовательно, для чего-то вроде memcpy, если вы не пишете что-то, что будет привязано к очень специфическому аппарату, и если вы не потратите время на создание высокооптимизированной функции memcpy в сборке ( или используя встроенные функции уровня C), вы намного лучше, чтобы компилятор мог понять это для вас.

Ответ 6

В качестве общего руководства memcpy():

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

b) Если скопированные данные малы (менее 4 KiB) и гарантированно выровнены, используйте rep movsb (если поддерживается ERMSB) или rep movsd (если ERMSB не поддерживается). Причина. Использование альтернативы SSE или AVX имеет огромное количество "начальных загрузок", прежде чем копирует что-либо.

c) Если скопированные данные малы (менее 4 KiB) и не гарантируется выравнивание, используйте rep movsb. Причина: использование SSE или AVX или использование rep movsd для большей части его плюс некоторые rep movsb в начале или в конце имеют слишком много накладных расходов.

d) Для всех остальных случаев используйте что-то вроде этого:

    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

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