Что здесь делает vmovdqu? - PullRequest
0 голосов
/ 16 мая 2018

У меня есть цикл Java, который выглядит следующим образом:

public void testMethod() {
    int[] nums = new int[10];
    for (int i = 0; i < nums.length; i++) {
        nums[i] = 0x42;
    }
} 

Сборка, которую я получаю, такова:

0x00000001296ac845: cmp    %r10d,%ebp
0x00000001296ac848: jae    0x00000001296ac8b4
0x00000001296ac84a: movl   $0x42,0x10(%rbx,%rbp,4)
0x00000001296ac852: inc    %ebp               
0x00000001296ac854: cmp    %r11d,%ebp
0x00000001296ac857: jl     0x00000001296ac845  

0x00000001296ac859: mov    %r10d,%r8d
0x00000001296ac85c: add    $0xfffffffd,%r8d
0x00000001296ac860: mov    $0x80000000,%r9d
0x00000001296ac866: cmp    %r8d,%r10d
0x00000001296ac869: cmovl  %r9d,%r8d
0x00000001296ac86d: cmp    %r8d,%ebp
0x00000001296ac870: jge    0x00000001296ac88e
0x00000001296ac872: vmovq  -0xda(%rip),%xmm0                                                    
0x00000001296ac87a: vpunpcklqdq %xmm0,%xmm0,%xmm0
0x00000001296ac87e: xchg   %ax,%ax

0x00000001296ac880: vmovdqu %xmm0,0x10(%rbx,%rbp,4)  
0x00000001296ac886: add    $0x4,%ebp          
0x00000001296ac889: cmp    %r8d,%ebp
0x00000001296ac88c: jl     0x00000001296ac880  

Если мое понимание верно, первый блок сборкитот, который делает nums[i] = 0x42;.В третьем блоке есть vmovdqu, который

Инструкция vmovdqu перемещает значения из целочисленного вектора в ячейку памяти без выравнивания.

Однако я до сих пор не до конца понимаю, что делает vmovdqu в контексте моего цикла.

Что именно делает третий блок кода сборки?

Полный код доступен здесь: https://pastebin.com/cT5cJcMS

Ответы [ 3 ]

0 голосов
/ 17 мая 2018

Компилятор развернул цикл, чтобы включить векторизацию.

// 10d holds the length of the array and ebp holds the loop index.
0x00000001296ac845: cmp    %r10d,%ebp
// This branch is only taken when the loop index `i` is larger or equal to `nums.length`.
0x00000001296ac848: jae    0x00000001296ac8b4
// Performs a single iteration.
0x00000001296ac84a: movl   $0x42,0x10(%rbx,%rbp,4)
// Increment the loop index.
0x00000001296ac852: inc    %ebp
// r11d contains some constant. This is just to ensure that the number of any remaining iterations is multiple of 4.             
0x00000001296ac854: cmp    %r11d,%ebp
// This branch is NOT taken (falls through) only when either zero iterations are left of when the number of remaining iterations is a multiple of 4.
0x00000001296ac857: jl     0x00000001296ac845  
// These instructions make sure that the loop index does not overflow.
0x00000001296ac859: mov    %r10d,%r8d
0x00000001296ac85c: add    $0xfffffffd,%r8d
0x00000001296ac860: mov    $0x80000000,%r9d
0x00000001296ac866: cmp    %r8d,%r10d
0x00000001296ac869: cmovl  %r9d,%r8d
// The next two instructions check whether there are any remaining iterations.
0x00000001296ac86d: cmp    %r8d,%ebp
0x00000001296ac870: jge    0x00000001296ac88e
// If we reach here, the number of remaining iterations must be a multiple of 4.
// Initialize xmm0 with 4 copies of 0x42.
0x00000001296ac872: vmovq  -0xda(%rip),%xmm0                                                    
0x00000001296ac87a: vpunpcklqdq %xmm0,%xmm0,%xmm0
// This is a NOP just to align the loop on a 64-byte cache line boundary for performance.
0x00000001296ac87e: xchg   %ax,%ax
// Vectorized 4 iterations of the loop.
0x00000001296ac880: vmovdqu %xmm0,0x10(%rbx,%rbp,4)  
0x00000001296ac886: add    $0x4,%ebp          
0x00000001296ac889: cmp    %r8d,%ebp
0x00000001296ac88c: jl     0x00000001296ac880
// All iterations have been executed at this point.
0 голосов
/ 17 мая 2018

Ваш JIT-компилятор автоматически векторизовал ваш цикл, сохраняя 4 int с на каждую итерацию.

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

Ваш код не возвращает nums, поэтому он уничтожается сразу после создания. После встраивания ваша функция должна оптимизироваться без инструкций. Или как отдельная функция, должна быть просто ret. Выделение памяти и последующая сборка мусора не является видимым побочным эффектом, который должен сохранить оптимизатор.

Тогда, если new удастся, тогда nums.length будет 10. Таким образом, код может быть таким простым, как

# %rbx holds a pointer to the actual data storage for nums[]
vbroadcastss  -0x????(%rip),%xmm0      # broadcast-load the 0x42 constant into xmm0
vmovdqu       %xmm0,   (%rbx)          # nums[0..3] : 16 bytes
vmovdqu       %xmm0, 16(%rbx)          # nums[4..7] : 16 bytes
vmovq         %xmm0, 32(%rbx)          # nums[8..9] : 8 bytes

Здесь наиболее целесообразно полностью развернуть цикл; настройка счетчиков циклов и т. д. требует больше инструкций и размера кода, чем парочка магазинов. Особенно, если размер не кратен ширине вектора, последний частичный вектор должен быть обработан специально в любом случае.

Кстати, если бы ваш размер был 11 вместо 10, вы могли бы делать 8 + 4-байтовые хранилища или 16-байтовые хранилища, которые частично перекрывались, например, 16-байтовые vmovdqu сохраняют до (%rbx), 16(%rbx) и 28(%rbx), охватывающие nums[7..11]. Последний невыровненный вектор, который заканчивается в конце массива, является обычной стратегией при ручной векторизации (или в обработке небольшого буфера в glibc для memcpy), но даже опережающие компиляторы, похоже, не используют его.


Другие очевидные пропущенные оптимизации:

  • vmovq загрузить + vpunpcklqdq для трансляции. При наличии AVX vbroadcastss - лучший способ транслировать 32-битную константу из памяти. Одна инструкция без ALU UOP требуется. Может, JIT-компилятор на самом деле не знает о новых инструкциях AVX?

  • mov %r10d,%r8d + add $-3,%r8d: это, очевидно, должно быть lea -3(%r10), %r8d.

Непонятно, какое предполагается начальное значение %ebp; если JVM где-то разделяет части буфера, поэтому RBX не является основой массива, то, возможно, EBP не равен 0 до скалярного цикла? IDK, почему граница цикла скалярного цикла находится в регистре, а не в непосредственном.

Странно размещать статические данные на той же странице, что и код (-0xda(%rip) все еще на той же странице). Там нет большого штрафа, но это означает, что одна и та же страница должна быть в iTLB и dTLB, поэтому вы покрываете меньше всего кода и данных, чем если бы вы использовали отдельные страницы. Не так уж и много с 2M огромных страниц. Общий TLB 2-го уровня представляет собой кэш-память жертвы (IIRC), поэтому заполнение его пропуском iTLB, вероятно, не поможет нагрузке vmovq получить удар TLB. Это, вероятно, сделает прогулку 2-ой страницы.


Я не знаю, почему даже хорошие опережающие компиляторы C, такие как gcc и clang, слишком сильно усложняют это для цикла по массиву с неизвестным выравниванием и длиной.

void set42(int *nums, unsigned long int len) {
    for (unsigned long int i=0 ; i<len ; i++ ) {
        *nums++ = 0x42;
    }
}

Это то, что я бы сделал вручную, для 128-битных векторов без развертывания цикла (и с оптимизмом предполагая, что не стоит достигать границы выравнивания, как ваш JIT, и как clang и gcc8 и выше):

# x86-64 System V calling convention: int*nums in RDI,  len in RSI
set42:
    cmp           $4, %rsi
    jb          .Lsmall_count

    lea          -16(%rdi, %rsi,4), %rdx    # pointer to end-16, the start of the final vector store
    vbroadcastss constant(%rip), %xmm0
 .p2align 4
 .Lvector:                          # do {
    vmovdqu      %xmm0, (%rdi)
    add          $16, %rdi          # nums += 4 elements
    cmp          %rdx, %rdi
    jb           .Lvector           # while(nums < end-16);

    # only reached for sizes >= 16 bytes so we can always store a full possibly-overlapping final vector
    # for len = 16, this results in 2 stores to the same address, but that's cheaper than extra branches even if len=16 is common
    vmovdqu      %xmm0, (%rdx)   # final potentially-overlapping vector
    ret

.Lsmall_count:
    test         %rsi,%rsi
    jz          .Ldone
   # some compilers will fully unroll this with a chain of branches
   # maybe worth doing if small inputs are common
  .Lscalar:                       # do {
    movl        0x42, (%rdi)
    add         $4, %rdi          # *num++ = 0x42;
    dec         %rsi
    jnz                           # }while(--len);
  # a more sophisticated cleanup strategy using SIMD is possible, e.g. 8-byte stores,
  # but I haven't bothered.

.Ldone:
    ret

Обратите внимание, что для len>=4 есть одна сквозная ветвь вверху, затем только ветвь цикла. Общее количество служебных данных составляет 1 cmp / jcc с макросов, 1 широковещательная загрузка и 1 lea. Цикл равен 3 моп с неиндексированным режимом адресации.

AFAIK, компиляторы не знают, как эффективно использовать возможно перекрывающийся последний вектор. Это намного лучше, чем скалярная очистка большую часть времени. Обратите внимание, что для len = 4 (16 байт) мы делаем одно и то же хранилище векторов дважды. Но для len = 8 (32 байта) цикл завершается после первой итерации, поэтому мы все еще делаем только 2 полных хранилища. то есть с любым точным кратным ширины вектора, отличным от 1, мы не делаем перекрывающийся магазин. Ветвление одинаково для len = 4 и len = 8 на самом деле хорошо для предсказания ветвления.


Даже хорошие опережающие компиляторы C делают это очень сложным, , как вы можете видеть в проводнике компилятора Godbolt .Некоторая сложность clang проистекает из разворачивания большего;clang6.0 раскатывается огромное количество раз.(Я выбрал версии и опции компилятора, которые привели к наименее сложному коду. Для этого gcc7.3 и clang6.0 испускают гораздо более крупные функции.)

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

0 голосов
/ 16 мая 2018

Оптимизатор выбрал векторизацию вашего цикла, установив 4 значения для «итерации».(Инструкции, предшествующие vmovdqu, довольно непрозрачны, но, по-видимому, он разбивает 0x42 на все дорожки XMM0.) Вариант «unaligned» необходим, потому что массив не гарантирует выравнивание SIMD в памяти (послевсе, это хранит int32 с, а не int32x4 с).

...