tgoop.com/cxx95/74
Last Update:
#compiler #madskillz
[[assume]] - помоги компилятору сам
Раньше я писал про std::unreachable
(он же __builtin_unreachable
до C++23) - https://www.tgoop.com/cxx95/58.
Эта штука делает указание компилятору, что в данную ветку исполнения программа никогда не попадет (под личную ответственность программиста), поэтому можно оптимизировать это место.
В C++23 по такому образу стандартизировали похожий функционал: атрибут [[assume(expr)]] (он же __builtin_assume
до C++23).
Эта штука делает указание компилятору, что в данной ветке исполнения выражение expr
следует считать равным true
, и делать разные оптимизации на основе этих данных. Выражение expr
вычисляться во время работы программы не будет, это подсказка времени компиляции.
На cppreference (ссылка выше) информации мало, лучше почитать "предложение" о стандартизации: https://wg21.link/p1774r8
Самый простой пример - метод, который делит число на 32:
int div32(int x) {Казалось бы, очевидная оптимизация - не делить на 32, а сделать битовый сдвиг на 5 битов:
return x / 32;
}
int div32(int x) {Но будет неправильно работать на отрицательных числах. Компилятор всегда должен учитывать возможность входа отрицательного числа, из-за этого метод больше по размеру: ссылка на godbolt.
return x >> 5;
}
Если программист совершенно точно знает, что все числа будут неотрицательными, то нужно сделать так:
int div32_2(int x) {И тогда код оптимизируется: ссылка на godbolt.
[[assume(x >= 0)]]; // или __builtin_assume(x >= 0);
return x / 32;
}
В более сложных примерах, которые показываются в "предложении", можно в несколько раз уменьшить количество инструкций ассемблера, особенно в математических программах.
Некоторые assume можно сделать общими для всего кода (в "предложении" есть пример с умными указателями), но в целом это вещь для узкого круга разработчиков. Есть несколько особенностей этой фичи:
void limiter(float* data, size_t size) {Предполагая, что размер буфера всегда больше 0 и кратен 32, а флоаты нормализованные, программист ставит assume.
[[assume(size > 0)]];
[[assume(size % 32 == 0)]];
for (size_t i = 0; i < size; ++i) {
[[assume(std::isfinite(data[i]))]];
data[i] = std::clamp(data[i], -1.0f, 1.0f);
}
}
Первый и третий assume не дает делать лишние проверки, а второй assume вероятно как-то связан с кэш-линией процессора.
Можно сделать разные приколы с
assume