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