"Есть некоротая вселенская несправедливость", — подумали в комитете стандартизации C++, — "мы так много всего в языке назначили быть неопределенным поведением, чтоб помочь компиляторам генерировать оптимальный код. Но не дали такую же стандартную возможность нашим пользователям — программистам!"
Да, С++23 наконец-то дал простым пользователям инструмент целенаправленного внедрения неопределенного поведения в их код. Такой инструмент, правда, давно уже был и так, но специфичный для конкретного компилятора. C++23 же всего лишь стандартизировал его. Так что радуйтесь, никаких больше уродливых __builtin_assume
!
- Зачем вообще такая возможность существует?! — первый же вопрос, который возникает после прочтения абзаца выше. Неужели недостаточно ужасов самого языка, нужно еще пользователям позволить создавать новые?!
На самом деле, конечно, причина есть: компиляторы глупые, быстрый и оптимальный код получить хочется, а на ассемблере писать не очень хочется. Хотя, конечно, разработчики ffmpeg с этим не согласятся — они поэтому целенаправленно делают ассемблерные вставки, не доверяя компиляторам С.
Несмотря на то что мы говорим о C и C++, я позволю себе привести пример на Rust, поскольку считаю, что он наиболее ярко может продемонстрировать логику новвовведения С++23.
Возьмем достаточно простую функцию, которая выполняет семплирование отсортированной выборки: разбивает ее на группы равной величины и из каждой группы выбирает медианну величину
use std::num::NonZeroUsize;
pub fn medians(data: &[f32], group: NonZeroUsize) -> Vec<f32> {
let n = group.get();
data.chunks_exact(n) // разбиваем на группы по n,
// последняя группа если в ней меньше n -- игнорируется
.map(move |chunk| chunk[n/2]) // берем медиану
.collect() // собираем результат
}
Если мы скомпилируем эту функцию довольно старой версией Rustc 1.51 с opt-level=3, мы обнаружим, что код получился так себе
- Мы видим в начале функции
sub rsp, 120
mov qword ptr [rsp + 96], rcx
test rcx, rcx
je .LBB4_33
...
.LBB4_33:
...
call qword ptr [rip + core::panicking::panic_fmt::hcd56f7f635f62c74@GOTPCREL]
ud2
Это проверка что n
не ноль. Но мы же и так знаем что n
не ноль — это четко указано в типе входного параметра!
- При обработке каждой группы мы находим
shr rdi
cmp rdi, r15
jae .LBB4_27
...
.LBB4_27:
lea rdx, [rip + .L__unnamed_4]
mov rsi, r15
call qword ptr [rip + core::panicking::panic_bounds_check::h16537cfb53a1364b@GOTPCREL]
Каждый раз проверяется что индекс n/2
в границах группы. Но ведь это всегда так!
Очень бы хотелось донести до компилятора такие очевидные факты. Собственно [[assume(condition)]]
для того в C++23 и добавили. Если компилятор не смог догадаться до чего-то самостоятельно и сгенерировать оптимальный код, мы теперь можем ему подсказать...
Так c GCC14 и C++26 та же самая функция (используя безопасные методы, как в Rust)
struct NonZero {
public:
explicit NonZero(size_t v) : value {
v > 0 ? v : throw std::runtime_error("Zero value")
} {}
size_t get() const {
return value;
}
private:
size_t value;
};
template <class T>
auto chunks_exact(std::span<T> data, size_t n) {
if (n == 0) {
throw std::runtime_error("zero chunk len");
}
return data.subspan(0, data.size() - data.size() % n)
| std::views::chunk(n)
| std::views::transform([](auto chunk){ return std::span(&chunk.front(), chunk.size()); }); // remap into spans
}
__attribute__((noinline))
std::vector<float> medians(std::span<const float> data, NonZero group) {
size_t n = group.get();
// [[assume(n>0)]];
return chunks_exact(data, n)
| std::views::transform([n](auto chunk) { return chunk.at(n/2); })
| std::ranges::to<std::vector>();
}
также компилируется со всеми ненужными проверками. Но стоит нам только лишь добавить [[assume(n>0)]]
, как ситуация меняется и все избыточные проверки на ноль и на границы групп могут быть успешно выброшены компилятором!
Но что если мы подсказали неправильно? Неопределенное поведение, конечно же! Из ложной посылки следует что угодно.
А если мы подсказывали правильно, но только на допустимом множестве входных данных? Отлично, все хорошо, только не забудьте включить в документацию упоминание неопределенного поведения на недопустимом входе.
Но прежде чем начинать пользоваться такой замечательной возможностью языка, стоит понимать:
- Правильная подсказка ничего не гарантирует. А ложная невероятно опасна
- Новые версии компиляторов и сами могут догадаться. Так, например, rustc 1.80 на рассмотренном примере оптимизирует уже все как надо.