Выполнение инструкций x86 rep на современных (конвейерных/суперскалярных) процессорах

Я писал в сборке x86 в последнее время (для удовольствия) и задавался вопросом, действительно ли префиксные префиксные инструкции действительно имеют преимущество производительности на современных процессорах или если они просто реализованы для обратной совместимости.

Я могу понять, почему Intel изначально выполняла инструкции rep, когда процессоры выполняли только одну команду за раз, но есть ли возможность использовать их сейчас?

С циклом, который компилируется для получения дополнительных инструкций, необходимо заполнить конвейер и/или выйти из строя. Современные процессоры, построенные для оптимизации этих префиксных инструкций, или команды rep, которые так редко используются в современном коде, что они не важны для производителей?

Ответ 1

В вопросах как в руководствах по оптимизации AMD, так и в Intel уделяется много внимания. Срок действия рекомендаций, приведенных в этой области, имеет "период полураспада" - разные поколения ЦП ведут себя по-разному, например:

В Руководстве по оптимизации архитектуры Intel приведены показатели производительности для различных методов копирования блоков (включая rep stosd) в таблице 7-2. Относительная производительность операций копирования памяти, стр. 7-37f., Для разных процессоров, и снова, что быстрей на одном, возможно, не было бы быстрее на других.

Во многих случаях последние процессоры x86 (которые имеют "строковые" операции SSE4.2) могут выполнять строковые операции через модуль SIMD, см. это расследование.

Чтобы следить за всем этим (и/или постоянно обновляться, когда ситуация меняется снова, неизбежно), читайте руководства/блоги оптимизации Agner Fog.

Ответ 2

В дополнение к FrankH отличный ответ; Я хотел бы указать, что какой метод лучше всего также зависит от длины строки, ее выравнивания и если длина фиксированная или переменная.

Для небольших строк (возможно, до 16 байт) выполнение этого вручную с помощью простых инструкций, вероятно, происходит быстрее, поскольку оно позволяет избежать затрат на установку более сложных методов (и для строк фиксированного размера можно легко развернуть). Для строк среднего размера (возможно, от 16 байт до 4 килобайт) что-то вроде "REP MOVSD" (с некоторыми инструкциями "MOVSB", которые могут быть выбраны, если смещение возможно), вероятно, будет лучше.

Для чего-то большего, чем у некоторых людей, у вас возникнет соблазн перейти на SSE/AVX и предварительную выборку и т.д. Лучшая идея - исправить вызывающего абонента, чтобы не копировать (или strlen() или что-то еще) в первую очередь. Если вы достаточно стараетесь, вы почти всегда найдете способ. Примечание. Также очень осторожно относитесь к "предполагаемым" быстрым процедурам mempcy() - как правило, они тестировались на массивных строках и не тестировались на гораздо более вероятных крошечных/малых/средних строках.

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

Ответ 3

Поскольку никто не дал вам никаких цифр, я дам вам некоторые, которые я нашел, сравнивая мой сборщик мусора, который очень тяжелый. Мои объекты, подлежащие копированию, составляют 60% 16 байтов, а остальные 30% - 500-8000 байтов или около того.

  • Условие: обе dst, src и n кратны 8.
  • Процессор: AMD Phenom (tm) II X6 1090T Процессор 64bit/linux

Вот мои три варианта memcpy:

Ручной код while:

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 выигрывает крошечный запас.