Чтобы мне было слегка дискомфортно, давайте эту неделю я попробую писать каждый день какие-то интересные, но возможно поменьше штуки.



Clang PGO (profile guided optimization) -- отличная тулза, чтобы ускорять программы по собранным профайлам в проде. Все браузеры собраны им, очень однообразные workflow отлично для этого подходят, в целом выигрыш стоит ожидать около 5-15%, например, в трейдинге очень любят эти оптимизации. Формат примерно такой:



1. Вы собираете бинарный файл с помощью GCC или Clang

2. Собираете perf record на бинарях

3. Перекомпилируете, а clang/gcc во всех циклах, if statement, которые может опознать, пытается понять, стоит ли там векторизовать код в зависимости от типичного размера цикла или как поставить блоки правильно в зависимости от ветвления кода.



Вот буквально вчера обнаружил такой сниппет в коде:



size_t DivUp(size_t n, size_t m) { return (n + m - 1) / m; }

size_t RoundUp(size_t n, size_t m) { return DivUp(n, m) * m; }



size_t RoundUpForTag(size_t size) {

return RoundUp(size, (size <= 512) ? 8 : (size <= 8192 ? 64 : 4096));

}



Казалось бы, здесь происходит округление по степеням двойки во вложенном тернарном операторе. Clang производит великолепный кодген, заменяя деление (что логично со степенями двойки) на (n + m - 1) & (~m + 1)





        cmpq    $8193, %rdi

movl $64, %eax

movl $4096, %ecx

cmovbq %rax, %rcx

cmpq $513, %rdi

movl $8, %eax

cmovaeq %rcx, %rax

leaq (%rdi,%rax), %rcx

decq %rcx

negq %rax

andq %rcx, %rax

retq



Теперь давайте попробуем собрать профиль на синтетике, где распределение размеров в тернарном операторе будет неравномерным. Код можно прочитать здесь.



Далее



# Компилируем

$ clang++ A.cpp -O3 -o test -std=c++17 -stdlib=libc++ -fno-exceptions -g -fprofile-instr-generate -gline-tables-only

# Собираем профайл

$ perf record -b ./test 123 12383 1238

# Конвертируем профиль

$ create_llvm_prof --binary=test --out=code.prof

# Компилируем с профилем

$ clang++ A.cpp -O3 -o test -std=c++17 -stdlib=libc++ -fno-exceptions -g -fprofile-instr-generate -gline-tables-only -fprofile-sample-use=code.prof



$ objdump -dCs --no-addresses ./test | grep div -C 5

mov %rcx,%rax

shr $0x20,%rax

je <RoundUpForTag(unsigned long)+0x74>

mov %rcx,%rax

xor %edx,%edx

div %rsi

sub %rdx,%rcx

mov %rcx,%rax

ret

mov %ecx,%eax

xor %edx,%edx

div %esi

sub %rdx,%rcx

mov %rcx,%rax

ret



Откуда, блин, деление? Деление занимает 30-40 тактов. Бенчмарк на моей машине стал на 12% медленнее.



Разгадка: если поставить LIKELY/UNLIKELY атрибуты, clang действительно генерирует div инструкцию

https://gcc.godbolt.org/z/WjW5f4dsK. Про gcc вообще молчу, там всё очень плохо



Сгорело.



Пример из очень серьёзной реальности, которой пользуются повсеместно -- absl::Cord.



Вот так микрооптимизируешь, а потом компиляторы сами тебе дают пощёчины. Ещё такой кодген очень тяжело обнаружить (так как он постпродакшн), нашёл просто случайно.



[1] Godbolt

[2] Пример из absl::Cord

[3] Синтетика