Files
Sergey Fukanchik 8c1499929a Fix a number of typos (#128)
Co-authored-by: Sergey Fukanchik <s.fukanchik@postgrespro.ru>
2025-09-29 15:13:45 +01:00

109 lines
7.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Атрибут [[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) уже все как надо.