mirror of
https://github.com/Nekrolm/ubbook.git
synced 2026-06-09 13:14:18 +03:00
8c1499929a
Co-authored-by: Sergey Fukanchik <s.fukanchik@postgrespro.ru>
109 lines
7.2 KiB
Markdown
109 lines
7.2 KiB
Markdown
# Атрибут [[assume]]
|
||
|
||
"Есть некоторая вселенская несправедливость", — подумали в комитете стандартизации C++, — "мы так много всего в языке назначили быть неопределенным поведением, чтоб помочь компиляторам генерировать оптимальный код. Но не дали такую же стандартную возможность нашим пользователям — программистам!"
|
||
|
||
Да, С++23 наконец-то дал простым пользователям инструмент целенаправленного **внедрения** неопределенного поведения в их код. Такой инструмент, правда, давно уже был и так, но специфичный для конкретного компилятора. C++23 же всего лишь стандартизировал его. Так что радуйтесь, никаких больше уродливых `__builtin_assume`!
|
||
|
||
- Зачем вообще такая возможность существует?! — первый же вопрос, который возникает после прочтения абзаца выше. Неужели недостаточно ужасов самого языка, нужно еще пользователям позволить создавать новые?!
|
||
|
||
На самом деле, конечно, причина есть: компиляторы глупые, быстрый и оптимальный код получить хочется, а на ассемблере писать не очень хочется. Хотя, конечно, разработчики ffmpeg с этим не согласятся — они поэтому целенаправленно делают ассемблерные вставки, не доверяя компиляторам С.
|
||
|
||
Несмотря на то что мы говорим о C и C++, я позволю себе привести пример на Rust, поскольку считаю, что он наиболее ярко может продемонстрировать логику нововведения С++23.
|
||
|
||
Возьмем достаточно простую функцию, которая выполняет семплирование отсортированной выборки: разбивает ее на группы равной величины и из каждой группы выбирает медианную величину
|
||
|
||
```Rust
|
||
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() // собираем результат
|
||
}
|
||
```
|
||
|
||
Если мы [скомпилируем](https://godbolt.org/z/vYezzv9ba) эту функцию довольно старой версией Rustc 1.51 с opt-level=3, мы обнаружим, что код получился так себе
|
||
|
||
1. Мы видим в начале функции
|
||
```
|
||
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` не ноль — это четко указано в типе входного параметра!
|
||
|
||
2. При обработке каждой группы мы находим
|
||
```
|
||
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)
|
||
|
||
```C++
|
||
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)]]`, как ситуация [меняется](https://godbolt.org/z/Yvez1WjeK) и все избыточные проверки на ноль и на границы групп могут быть успешно выброшены компилятором!
|
||
|
||
---
|
||
|
||
Но что если мы подсказали неправильно? Неопределенное поведение, конечно же!
|
||
Из ложной посылки следует что угодно.
|
||
|
||
А если мы подсказывали правильно, но только на допустимом множестве входных данных? Отлично, все хорошо, только не забудьте включить в документацию упоминание неопределенного поведения на недопустимом входе.
|
||
|
||
Но прежде чем начинать пользоваться такой замечательной возможностью языка, стоит понимать:
|
||
|
||
1. Правильная подсказка ничего не гарантирует. А ложная невероятно опасна
|
||
2. Новые версии компиляторов и сами могут догадаться. Так, например, rustc 1.80 на рассмотренном примере [оптимизирует](https://godbolt.org/z/c5Kc9hP8r) уже все как надо.
|
||
|