Подтвердить что ты не робот

Почему один из этих sooooo намного быстрее, чем другой?

Я пишу код на С++, чтобы найти первый байт в памяти, который не равен 0xFF. Чтобы использовать битки, я написал встроенный код сборки, который мне очень нравится. Но для "удобочитаемости", а также для будущей проверки (т.е. Векторизации SIMD) я думал, что я дам g++ optimizer шанс. g++ не векторизует, но он дошел почти до того же решения, что и без SIMD. Но по какой-то причине его версия работает намного медленнее, на 260000x медленнее (то есть мне нужно зацикливать мою версию на 260 000 раз больше, чтобы добраться до того же времени выполнения). Я исключил некоторую разницу, но не ЭТО много! Может ли кто-нибудь указать, почему это может быть? Я просто хочу знать, чтобы совершить ошибку в будущих встроенных ассемблерных кодах.

Начальная точка С++ следующая (с точки зрения точности подсчета в этом коде есть ошибка, но я упростил ее для этого теста скорости):

uint64_t count3 (const void *data, uint64_t const &nBytes) {
      uint64_t count = 0;
      uint64_t block;
      do {
         block = *(uint64_t*)(data+count);
         if ( block != (uint64_t)-1 ) {
/*       count += __builtin_ctz(~block);   ignore this for speed test*/
            goto done;
          };
        count += sizeof(block);
      } while ( count < nBytes );
done:
      return (count>nBytes ? nBytes : count);
}

Сборочный код g++ появился:

_Z6count3PKvRKm:
.LFB33:
    .cfi_startproc
    mov rdx, QWORD PTR [rsi]
    xor eax, eax
    jmp .L19
    .p2align 4,,10
    .p2align 3
.L21:
    add rax, 8
    cmp rax, rdx
    jnb .L18
.L19:
    cmp QWORD PTR [rdi+rax], -1
    je  .L21
.L18:
    cmp rax, rdx
    cmova   rax, rdx
    ret
    .cfi_endproc

Моя встроенная сборка

_Z6count2PKvRKm:
.LFB32:
    .cfi_startproc
    push    rbx
    .cfi_def_cfa_offset 16
    .cfi_offset 3, -16
    mov rbx, QWORD PTR [rsi]

    # count trailing bytes of 0xFF 
    xor     rax, rax  
.ctxff_loop_69:          
    mov     r9,  QWORD PTR [rdi+rax] 
    xor     r9, -1          
    jnz   .ctxff_final_69    
    add     rax, 8     
    cmp     rax, rbx 
    jl    .ctxff_loop_69    
.ctxff_final_69:         
    cmp     rax,rbx  
    cmova   rax,rbx  
    pop rbx
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc

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

Можно предположить, что мой метод тестирования вызывает ошибку, но все, что я делаю, это изменение имени функции и длины итерации в следующем, простом для цикла, показанном ниже: (когда N равно 1 < 20 и всем байтам 'a' кроме последнего байта 0xFF)

test 1

   for (uint64_t i=0; i < ((uint64_t)1<<15); i++) {
      n = count3(a,N);
   }

test 2

   for (uint64_t i=0; i < ((uint64_t)1<<33); i++) {
      n = count2(a,N);
   }

EDIT:

Вот мои реальные встроенные ассемблерные коды с SSE count1(), x64-64 count(), а затем версии с открытым кодом - С++ версии count0() и count3(). Я упал в эту кроличью нору, надеясь, что я смогу получить g++, чтобы взять мой count0() и приступить к нему по собственному усмотрению к count1() или даже count2(). Но, увы, он ничего не сделал, абсолютно не optmization:( Я должен добавить, что у моей платформы нет AVX2, поэтому я надеялся получить g++ для автоматического векторизации, чтобы код автоматически обновлялся при обновлении моей платформы.

В терминах использования явного реестра в встроенной сборке, если я не сделал их явно, g++ повторно использовал те же регистры для nBytes и count.

Что касается ускорения, между XMM и QWORD, я нашел, что реальная выгода - это просто эффект "loop-unroll", который я реплицирую в count2().

uint32_t count0(const uint8_t *data, uint64_t const &nBytes) {

  for (int i=0; i<nBytes; i++)
    if (data[i] != 0xFF) return i;

  return nBytes;
}
uint32_t count1(const void *data, uint64_t const &nBytes) {
  uint64_t count;
  __asm__("# count trailing bytes of 0xFF \n"
    "   xor     %[count], %[count]  \n"
    " vpcmpeqb  xmm0, xmm0, xmm0  \n" // make array of 0xFF

    ".ctxff_next_block_%=:        \n"
    " vpcmpeqb  xmm1, xmm0, XMMWORD PTR [%[data]+%[count]]  \n"
    " vpmovmskb r9, xmm1         \n"
    " xor     r9, 0xFFFF       \n" // test if all match (bonus negate r9)
    " jnz   .ctxff_tzc_%=        \n" // if !=0, STOP & tzcnt negated r9
    " add     %[count], 16       \n" // else inc
    " cmp     %[count], %[nBytes] \n"
    " jl    .ctxff_next_block_%=  \n" // while count < nBytes, loop
    " jmp   .ctxff_done_%=      \n" // else done + ALL bytes were 0xFF

    ".ctxff_tzc_%=:           \n"
    " tzcnt   r9, r9          \n" // count bytes up to non-0xFF
    " add     %[count], r9    \n"

    ".ctxff_done_%=:          \n" // more than 'nBytes' could be tested,
    " cmp     %[count],%[nBytes]  \n" // find minimum
    " cmova   %[count],%[nBytes]  "
    : [count] "=a" (count)
    : [nBytes] "b" (nBytes), [data] "d" (data)
    : "r9", "xmm0", "xmm1"
  );
  return count;
};

uint64_t count2 (const void *data, uint64_t const &nBytes) {
    uint64_t count;
  __asm__("# count trailing bytes of 0xFF \n"
    "    xor     %[count], %[count]  \n"

    ".ctxff_loop_%=:          \n"
    "    mov     r9,  QWORD PTR [%[data]+%[count]] \n"
    "    xor     r9, -1          \n" 
    "    jnz   .ctxff_final_%=    \n"
    "    add     %[count], 8     \n" 
    "    mov     r9,  QWORD PTR [%[data]+%[count]] \n"  // <--loop-unroll
    "    xor     r9, -1          \n" 
    "    jnz   .ctxff_final_%=    \n"
    "    add     %[count], 8     \n" 
    "    cmp     %[count], %[nBytes] \n"
    "    jl    .ctxff_loop_%=    \n"
    "    jmp   .ctxff_done_%=   \n" 

    ".ctxff_final_%=:            \n"
    "    bsf   r9,  r9           \n" // do tz count on r9 (either of first QWORD bits or XMM bytes)
    "    shr     r9,  3          \n" // scale BSF count accordiningly
    "    add     %[count], r9    \n"
    ".ctxff_done_%=:          \n" // more than 'nBytes' bytes could have been tested,
    "    cmp     %[count],%[nBytes]  \n" // find minimum of count and nBytes
    "    cmova   %[count],%[nBytes]  "
    : [count] "=a" (count)
    : [nBytes] "b" (nBytes), [data] "D" (data)
    : "r9"
  );
  return count;
}

inline static uint32_t tzcount(uint64_t const &qword) {
  uint64_t tzc;
  asm("tzcnt %0, %1" : "=r" (tzc) : "r" (qword) );
  return tzc;
};

uint64_t count3 (const void *data, uint64_t const &nBytes) {
      uint64_t count = 0;
      uint64_t block;
      do {
        block = *(uint64_t*)(data+count);
         if ( block != (uint64_t)-1 ) {
           count += tzcount(~block);
            goto done;
          };
        count += sizeof(block);
      } while ( count < nBytes );
done:
      return (count>nBytes ? nBytes : count);
}

uint32_t N = 1<<20;

int main(int argc, char **argv) {

  unsigned char a[N];
  __builtin_memset(a,0xFF,N);

  uint64_t n = 0, j;
   for (uint64_t i=0; i < ((uint64_t)1<<18); i++) {
      n += count2(a,N);
   }

 printf("\n\n %x %x %x\n",N, n, 0);   
  return n;
}
4b9b3361

Ответ 1

Ответ на заголовок вопроса

Теперь, когда вы разместили полный код: вызов count2(a,N) выведен из цикла в main. Время работы по-прежнему очень незначительно увеличивается с количеством циклов (например, 1<<18), но все, что делает цикл, это одиночный add. Компилятор оптимизирует его, чтобы больше походить на этот источник:

uint64_t hoisted_count = count2(a,N);
for (uint64_t i=0; i < ((uint64_t)1<<18); i++) {
   n += hoisted_count;   // doesn't optimize to a multiply
}

Не существует конфликта регистров: %rax содержит результат инструкции asm, заключенный в строку count2. Затем он используется как исходный операнд в крошечном цикле, который умножает его на n на повторное добавление.

(см. asm в Godbolt Compiler Explorer, и обратите внимание на все предупреждения компилятора об арифметике на void* s: clang отказывается скомпилируйте свой код):

## the for() loop in main, when using count2()
.L23:
    addq    %rax, %r12
    subq    $1, %rdx
    jne     .L23

%rdx здесь счетчик циклов, а %r12 - это аккумулятор, который содержит n. IDK, почему gcc не оптимизирует его при умножении на постоянное время.

Предположительно, версия, которая была в 260 тыс. раз медленнее, не смогла вытащить весь count2 из цикла. С точки зрения gcc, версия inline asm намного проще: оператор asm рассматривается как чистая функция его входов, и gcc даже не знает ничего об этом, касаясь памяти. Версия C затрагивает кучу памяти, и гораздо сложнее доказать, что ее можно поднять.

Использование clobber "memory" в выражении asm не позволяло ему подниматься, когда я проверял godbolt. Вы можете указать из наличия или отсутствия цели ветвления в main перед векторным блоком.

Но в любом случае время выполнения будет выглядеть как n + rep_count vs. n * rep_count.

Оператор asm не использует привязку "memory" clobber или любые входы памяти, чтобы сообщить gcc, что он считывает память, на которую указывают указатели на вход. Возможны некорректные оптимизации. будучи выведенным из цикла, который модифицировал элементы массива. (См. Раздел раздел Clobbers в руководстве на примере использования ввода в память немого анонимного struct вместо обложки "memory" clobber. К сожалению, я не думаю, что это можно использовать, когда блок памяти не имеет постоянного времени компиляции.)

Я думаю, что -fno-inline предотвращает подъем, потому что функция не помечена __attribute__((const)) или немного слабее __attribute__((pure)), чтобы указать отсутствие побочных эффектов. После встраивания оптимизатор может видеть, что для оператора asm.


count0 не оптимизируется ни к чему хорошему, потому что gcc и clang не могут автоинъекционировать циклы, где число итераций неизвестно в начале. то есть они сосут на таких вещах, как strlen или memchr, или поисковые петли вообще, даже если им говорят, что они безопасны для доступа к памяти за пределами точки, в которой цикл поиска выходит раньше (например, используя char buf[static 512] как функция arg).


Оптимизация для вашего кода asm:

Как я прокомментировал вопрос, использование xor reg, 0xFFFF/jnz глупо по сравнению с cmp reg, 0xFFFF/jnz, потому что cmp/jcc может с помощью макросов встраивать uop. cmp reg, mem/jne также может сглаживаться с помощью макросов, поэтому скалярная версия, которая выполняет загрузку /xor/branch, использует 3x для каждого сравнения. (Разумеется, Sandybridge может только скомпенсировать загрузку, если он не использует режим индексированной адресации. Кроме того, SnB может только скомпенсировать только одну пару на блок декодирования, но вы, вероятно, получите первый cmp/jcc и ветвь цикла к макро-предохранителю.) В любом случае, xor - плохая идея. Лучше всего xor прямо перед tzcnt, поскольку сохранение в этом цикле более важно, чем размер кода или общий объем.

Ваша скалярная петля - это 9 скомпилированных доменов, которые слишком много для выдачи на одной итерации за 2 такта. (SnB - это 4-х широкий конвейер, и для крошечных циклов он может фактически поддерживать это.)


Отступы в коде в первой версии вопроса с count += __builtin_ctz на том же уровне, что и if, заставляют меня думать, что вы считали блоки рассогласования, а не просто находили первый.

К сожалению, код asm, который я написал для первой версии этого ответа, не решает ту же проблему, что и обновленный и понятный код OP. См. Старую версию этого ответа для SSE2 asm, которая подсчитывает байты 0xFF с использованием pcmpeqb/paddb и psadbw для горизонтальной суммы, чтобы избежать обхода.


Получение ускорения с помощью SSE2 (или AVX):

Ветвление на результат a pcmpeq занимает гораздо больше, чем разветвление на cmp. Если наш массив поиска велик, мы можем использовать цикл, который проверяет сразу несколько векторов, а затем выясняет, какой из байтов имел наш хит после выхода из цикла.

Эта оптимизация применяется и к AVX2.

Здесь моя попытка, используя GNU C inline asm с синтаксисом -masm=intel. (Intrinsics может дать лучшие результаты, особенно при встраивании, потому что компилятор понимает intrinsics и поэтому может делать постоянное распространение через них и т.д. OTOH, вы можете часто бить компилятор рукописным asm, если понимаете сделку -offs и микроархитектуры, на которую вы нацеливаете. Кроме того, если вы можете смело сделать некоторые предположения, но вы не можете легко передать их компилятору.)

#include <stdint.h>
#include <immintrin.h>

// compile with -masm=intel
// len must be a multiple of 32  (TODO: cleanup loop)
// buf should be 16B-aligned for best performance
size_t find_first_zero_bit_avx1(const char *bitmap, size_t len) {
    // return size_t not uint64_t.  This same code works in 32bit mode, and in the x32 ABI where pointers are 32bit

    __m128i pattern, vtmp1, vtmp2;
    const char *result_pos;
    int tmpi;

    const char *bitmap_start = bitmap;

    asm (  // modifies the bitmap pointer, but we're inside a wrapper function
      "vpcmpeqw   %[pat], %[pat],%[pat]\n\t"          // all-ones

      ".p2align 4\n\t"   // force 16B loop alignment, for the benefit of CPUs without a loop buffer
      //IACA_START  // See the godbolt link for the macro definition
      ".Lcount_loop%=:\n\t"
//      "  movdqu    %[v1], [ %[p] ]\n\t"
//      "  pcmpeqb   %[v1], %[pat]\n\t"        // for AVX: fold the load into vpcmpeqb, making sure to still use a one-register addressing mode so it can micro-fuse
//      "  movdqu    %[v2], [ %[p] + 16 ]\n\t"
//      "  pcmpeqb   %[v2], %[pat]\n\t"

      "  vpcmpeqb  %[v1], %[pat], [ %[p] ]\n\t"  // Actually use AVX, to get a big speedup over the OP scalar code on his SnB CPU
      "  vpcmpeqb  %[v2], %[pat], [ %[p] + 16 ]\n\t"

      "  vpand     %[v2], %[v2], %[v1]\n\t"         // combine the two results from this iteration
      "  vpmovmskb  %k[result], %[v2]\n\t"
      "  cmp       %k[result], 0xFFFF\n\t"          // k modifier: eax instead of rax
      "  jne     .Lfound%=\n\t"

      "  add       %[p], 32\n\t"
      "  cmp       %[p], %[endp]\n\t"              // this is only 2 uops after the previous cmp/jcc.  We could re-arrange the loop and put the branches farther apart if needed.  (e.g. start with a vpcmpeqb outside the loop, so each iteration actually sets up for the next)
      "  jb     .Lcount_loop%=\n\t"
      //IACA_END

      // any necessary code for the not-found case, e.g. bitmap = endp
      "  mov     %[result], %[endp]\n\t"
      "  jmp    .Lend%=\n\t"

      ".Lfound%=:\n\t"                       // we have to figure out which vector the first non-match was in, based on v1 and (v2&v1)
                                  // We could just search the bytes over again, but we don't have to.
                                  // we could also check v1 first and branch, instead of checking both and using a branchless check.
      "  xor       %k[result], 0xFFFF\n\t"
      "  tzcnt     %k[result], %k[result]\n\t"  // runs as bsf on older CPUs: same result for non-zero inputs, but different flags.  Faster than bsf on AMD
      "  add       %k[result], 16\n\t"          // result = byte count in case v1 is all-ones.  In that case, v2&v1 = v2

      "  vpmovmskb %k[tmp], %[v1]\n\t"
      "  xor       %k[tmp], 0xFFFF\n\t"
      "  bsf       %k[tmp], %k[tmp]\n\t"        // bsf sets ZF if its *input* was zero.  tzcnt flag results are based on its output.  For AMD, it would be faster to use more insns (or a branchy strategy) and avoid bsf, but Intel has fast bsf.
      "  cmovnz    %k[result], %k[tmp]\n\t"     // if there was a non-match in v1, use it instead of tzcnt(v2)+16

      "  add       %[result], %[p]\n\t"         // If we needed to force 64bit, we could use %q[p].  But size_t should be 32bit in the x32 ABI, where pointers are 32bit.  This is one advantage to using size_t over uint64_t
      ".Lend%=:\n\t"
      : [result] "=&a" (result_pos),   // force compiler to pic eax/rax to save a couple bytes of code-size from the special cmp eax, imm32  and xor eax,imm32 encodings
        [p] "+&r" (bitmap),
        // throw-away outputs to let the compiler allocate registers.  All early-clobbered so they aren't put in the same reg as an input
        [tmp] "=&r" (tmpi),
        [pat] "=&x" (pattern),
        [v1] "=&x" (vtmp1), [v2] "=&x" (vtmp2)
      : [endp] "r" (bitmap+len)
        // doesn't compile: len isn't a compile-time constant
        // , "m" ( ({ struct { char x[len]; } *dummy = (typeof(dummy))bitmap ; *dummy; }) )  // tell the compiler *which* memory is an input.
      : "memory" // we read from data pointed to by bitmap, but bitmap[0..len] isn't an input, only the pointer.
    );

    return result_pos - bitmap_start;
}

Этот фактически компилирует и собирает в asm, который выглядит так, как я ожидал, но я не тестировал его. Обратите внимание, что он оставляет все распределение регистров компилятору, поэтому он более дружественный к inlining. Даже без инкрустации он не заставляет использовать регистр с сохранением вызова, который должен быть сохранен/восстановлен (например, использование ограничения "b").

Не сделано: скалярный код для обработки последнего куска данных под-32B.

статический перфорированный анализ для процессоров Intel SnB-семейства на основе Agner Fog guide/tables. См. Также теги wiki. Я предполагаю, что мы не являемся узким местом по пропускной способности кеша, поэтому этот анализ применяется только тогда, когда данные горячие в кэше L2, или, может быть, только кеш L1 достаточно быстро.

Этот цикл может выходить из интерфейсного интерфейса на одной итерации (два вектора) на 2 такта, потому что он имеет 7 плавных доменов. (Внешние проблемы в группах по 4). (Вероятно, на самом деле это 8 uops, если две пары cmp/jcc декодируются в одном блоке. Haswell и более поздние могут выполнять два макро-слияния на группу декодирования, но предыдущие процессоры могут только сначала скомпилировать макрос. цикл, так что ранняя ветвь находится дальше от ветки p < endp.)

Все эти утипы с объединенными доменами включают в себя ALU uop, поэтому узкое место будет на портах выполнения ALU. Хасуэлл добавил четвертый блок ALU, который может обрабатывать простые не-векторные операционные системы, включая ветки, поэтому может запускать этот цикл на одной итерации на 2 такта (16B за такт). Ваш i5-2550k (упомянутый в комментариях) является процессором SnB.

Я использовал IACA для подсчета количества оборотов на порт, поскольку для этого требуется много времени. IACA тупой и думает, что существует какая-то зависимость между итерациями, отличная от счетчика циклов, поэтому мне пришлось использовать -no_interiteration:

g++ -masm=intel -Wall -Wextra -O3 -mtune=haswell find-first-zero-bit.cpp -c -DIACA_MARKS
iaca -64 -arch IVB -no_interiteration find-first-zero-bit.o

Intel(R) Architecture Code Analyzer Version - 2.1
Analyzed File - find-first-zero-bit.o
Binary Format - 64Bit
Architecture  - SNB
Analysis Type - Throughput

Throughput Analysis Report
--------------------------
Block Throughput: 2.50 Cycles       Throughput Bottleneck: Port1, Port5

Port Binding In Cycles Per Iteration:
-------------------------------------------------------------------------
|  Port  |  0   -  DV  |  1   |  2   -  D   |  3   -  D   |  4   |  5   |
-------------------------------------------------------------------------
| Cycles | 2.0    0.0  | 2.5  | 1.0    1.0  | 1.0    1.0  | 0.0  | 2.5  |
-------------------------------------------------------------------------

N - port number or number of cycles resource conflict caused delay, DV - Divider pipe (on port 0)
D - Data fetch pipe (on ports 2 and 3), CP - on a critical path
F - Macro Fusion with the previous instruction occurred
* - instruction micro-ops not bound to a port
^ - Micro Fusion happened
# - ESP Tracking sync uop was issued
@ - SSE instruction followed an AVX256 instruction, dozens of cycles penalty is expected
! - instruction not supported, was not accounted in Analysis

| Num Of |              Ports pressure in cycles               |    |
|  Uops  |  0  - DV  |  1  |  2  -  D  |  3  -  D  |  4  |  5  |    |
---------------------------------------------------------------------
|   2^   |           | 1.0 | 1.0   1.0 |           |     |     | CP | vpcmpeqb xmm1, xmm0, xmmword ptr [rdx]
|   2^   |           | 0.6 |           | 1.0   1.0 |     | 0.4 | CP | vpcmpeqb xmm2, xmm0, xmmword ptr [rdx+0x10]
|   1    | 0.9       | 0.1 |           |           |     | 0.1 | CP | vpand xmm2, xmm2, xmm1
|   1    | 1.0       |     |           |           |     |     |    | vpmovmskb eax, xmm2
|   1    |           |     |           |           |     | 1.0 | CP | cmp eax, 0xffff
|   0F   |           |     |           |           |     |     |    | jnz 0x18
|   1    | 0.1       | 0.9 |           |           |     |     | CP | add rdx, 0x20
|   1    |           |     |           |           |     | 1.0 | CP | cmp rdx, rsi
|   0F   |           |     |           |           |     |     |    | jb 0xffffffffffffffe1

В SnB: pcmpeqb может работать на p1/p5. Сплавленная слияния и ветвления может работать только на p5. Неплавкий cmp может работать на p015. Во всяком случае, если одна из ветвей не является макроблоком, цикл может работать на одной итерации за 8/3 = 2.666 циклов. С макро-fusion наилучшим случаем является 7/3 = 2.333 цикла. (IACA не пытается имитировать распределение uops на порты точно так же, как аппаратное обеспечение будет динамически принимать эти решения. Однако мы не можем ожидать идеального планирования с аппаратного обеспечения, поэтому 2 вектора на 2,5 цикла, вероятно, разумны как с макросами -fusions.Uops, которые могли бы использовать port0, иногда украдут порт1 или порт5, уменьшая пропускную способность.)

Как я уже говорил, Хасуэлл лучше справляется с этой петлей. IACA считает, что HSW может запускать цикл на одной итерации на 1.75c, но это явно неправильно, потому что принятая петля-ветвь завершает группу проблем. Он будет выдаваться в повторяющемся шаблоне 4,3 мк. Но исполнительные блоки могут обрабатывать большую пропускную способность, чем интерфейс для этого цикла, поэтому он действительно должен быть в состоянии идти в ногу с интерфейсом на Haswell/Broadwell/Skylake и работать на одной итерации за 2 такта.

Дальнейшее разворачивание большего количества vpcmpeqb/vpand составляет всего 2 мкп на вектор (или 3 без AVX, где мы будем загружать на царапину, а затем использовать это как пункт назначения для pcmpeqb.) Таким образом, при достаточной развертке, мы должны иметь возможность делать 2 векторных нагрузки за такт. Без AVX это было бы невозможно без трюка PAND, так как векторная загрузка/сравнение/movmsk/test-and-branch - 4 раза. Большие разматывания делают больше работы для декодирования конечной позиции, где мы нашли совпадение: скалярный цикл очистки cmp может быть хорошей идеей, когда мы находимся в этом районе. Возможно, вы можете использовать один и тот же скалярный цикл для очистки не-множественных размеров 32B.

Если вы используете SSE, с movdqu/pcmpeqb xmm,xmm, мы можем использовать режим индексированной адресации, не затрачивая нас на расчеты, потому что загрузка movdqu всегда представляет собой единую нагрузку, независимо от режима адресации. (В отличие от магазина нет необходимости в микроплавлении). Это позволяет нам сохранить нуль накладных расходов цикла, используя базовый указатель, указывающий на конец массива, и индекс, отсчитывающий от нуля. например add %[idx], 32/js, пока индекс отрицательный.

С AVX, однако, мы можем сохранить 2 uops с использованием режима однократной адресации, поэтому vpcmpeqb %[v1], %[pat], [ %[p] + 16 ] может быть микро-предохранителем. Это означает, что нам нужна структура цикла add/cmp/jcc, которую я использовал в примере. То же самое относится к AVX2.

Ответ 2

Поэтому я думаю, что нашел проблему. Я думаю, что один из регистров, используемых в моей встроенной сборке, несмотря на список clobber, противоречил g++ их использованию и искажал тестовую итерацию. Я передал g++ версию кода, вернувшись как встроенный код сборки и получил то же ускорение 260000x, что и мое собственное. Кроме того, ретроспективно, "ускоренное" время вычисления было абсурдно коротким.

Наконец, я был так сосредоточен на коде, воплощенном как функция, которую я не заметил, что g++ фактически лидировал (я использовал оптимизацию -O3) в тесте for-loop. Когда я принуждал g++ не к строке (т.е. -fno-inline), ускорение 260000x исчезло.

Я думаю, что g++ не смог принять во внимание встроенный ассемблерный код "список clobber", когда он встраивал всю функцию без моего разрешения.

Извлеченный урок. Мне нужно делать лучше на встроенных ограничениях сборки или блокировать встроенную функцию с помощью __attribute__ ((noinline))

EDIT: Определенно установлено, что g++ использует rax для счетчика main() for-loop, в конфликте с моим использованием rax.