Выполнение инструкций x86 rep на современных (конвейерных / суперскалярных) процессорах
Я писал в сборке x86 в последнее время (для забавы), и мне было интересно, действительно ли строковые инструкции с префиксом rep на самом деле имеют преимущество в производительности на современных процессорах или они просто реализованы для обратной совместимости.
Я могу понять, почему Intel изначально внедрила инструкции rep, когда процессоры выполняли только одну инструкцию за раз, но есть ли преимущество в их использовании сейчас?
С циклом, который компилирует больше инструкций, есть больше, чтобы заполнить конвейер и / или быть выведенным из строя. Созданы ли современные процессоры для оптимизации этих инструкций с префиксом, или команды репов используются так редко в современном коде, что они не важны для производителей?
3 ответа
В руководствах по оптимизации AMD и Intel много места отводится таким вопросам. Срок действия рекомендаций, данных в этой области, имеет "период полураспада" - разные поколения процессоров ведут себя по-разному, например:
- Руководство по оптимизации программного обеспечения AMD (сентябрь 2005 г.), раздел 8.3, стр. 167:
Избегайте использования префикса REP при выполнении строковых операций, особенно при копировании блоков памяти. - Руководство по оптимизации программного обеспечения AMD (апрель 2011 г.), раздел 9.3, стр. 148:
Используйте префикс REP разумно при выполнении строковых операций.
В Руководстве по оптимизации архитектуры Intel приведены сравнительные показатели производительности для различных методов блочного копирования (включая rep stosd
) в таблице 7-2. Относительная производительность процедур копирования памяти, стр. 7-37f., Для разных процессоров, и опять же, то, что является самым быстрым на одном, может быть не самым быстрым на других.
Во многих случаях последние процессоры x86 (которые имеют "строковые" операции SSE4.2) могут выполнять строковые операции через модуль SIMD, см. Это исследование.
Чтобы узнать обо всем этом (и / или держать себя в курсе, когда что-то неизбежно изменится), прочитайте руководства / блоги Agner Fog по оптимизации.
В дополнение к отличному ответу FrankH; Я хотел бы отметить, что какой метод является лучшим, зависит также от длины строки, ее выравнивания и от того, является ли длина фиксированной или переменной.
Для небольших строк (возможно, до около 16 байт) выполнение этого вручную с помощью простых инструкций, вероятно, быстрее, поскольку это позволяет избежать затрат на настройку более сложных методов (а для строк фиксированного размера можно легко развернуть). Для строк среднего размера (может быть от 16 байт до 4 КиБ) лучше всего подойдет что-то вроде "REP MOVSD" (с некоторыми инструкциями "MOVSB", если возможно смещение).
Для чего-то большего, чем это, некоторые люди будут испытывать желание перейти в SSE/AVX и предварительную выборку и т. Д. Лучшая идея состоит в том, чтобы исправить вызывающую / ие так, чтобы копирование (или strlen() или что-то еще) не требовалось в первом место. Если вы будете стараться изо всех сил, вы почти всегда найдете способ. Примечание: Также будьте очень осторожны с "предполагаемыми" процедурами fast mempcy() - обычно они тестировались на массивных строках и не тестировались на гораздо более вероятных крошечных / маленьких / средних строках.
Также обратите внимание, что (с целью оптимизации, а не удобства) из-за всех этих различий (вероятная длина, выравнивание, фиксированный или переменный размер, тип процессора и т. Д.) Идея иметь один универсальный "memcpy()" для всех очень разные случаи близоруки.
Так как никто не дал вам никаких чисел, я дам вам некоторые, которые я нашел, сравнив мой сборщик мусора, который очень тяжелый. Мои объекты, которые нужно скопировать, имеют длину 60% и 16 байт, а остальные 30% составляют 500 - 8000 байт или около того.
- Условие: оба
dst
,src
а такжеn
кратны 8. - Процессор: AMD Phenom(tm) II X6 1090T Процессор 64bit/linux
Вот мои три memcpy
варианты:
Ручная кодировка цикла
if (n == 16) {
*dst++ = *src++;
*dst++ = *src++;
} else {
size_t n_ptrs = n / sizeof(ptr);
ptr *end = dst + n_ptrs;
while (dst < end) {
*dst++ = *src++;
}
}
(ptr
псевдоним uintptr_t
). Время: 101,16%
rep movsb
if (n == 16) {
*dst++ = *src++;
*dst++ = *src++;
} else {
asm volatile("cld\n\t"
"rep ; movsb"
: "=D" (dst), "=S" (src)
: "c" (n), "D" (dst), "S" (src)
: "memory");
}
Время: 103,22%
rep movsq
if (n == 16) {
*dst++ = *src++;
*dst++ = *src++;
} else {
size_t n_ptrs = n / sizeof(ptr);
asm volatile("cld\n\t"
"rep ; movsq"
: "=D" (dst), "=S" (src)
: "c" (n_ptrs), "D" (dst), "S" (src)
: "memory");
}
Время: 100,00%
req movsq
выигрывает с небольшим отрывом.