group-telegram.com/dereference_pointer_there/4196
Last Update:
#prog #cpp #моё
В C++ есть такая вещь, как strict aliasing. Если коротко, то это предположение компилятора о том, что доступы по указателям (и ссылкам) существенно разных типов не пересекаются между собой. Подробнее про это можно прочитать, например, тут, ну а я покажу, как это влияет на (не)возможность оптимизировать код на C++. Все приведённые ниже примеры будут использовать компилятор GCC 12.2 с флагами --std=c++20 -O2 -pedantic
(с -O3
компилятор векторизует код и делает его гораздо объёмнее и менее понятным).
Напишем вот такой код (где std::span<int>
играет примерно ту же роль, что и &mut [i32]
в Rust):#include <span>
(в дальнейшем для экономии места я буду опускать
void increment(std::span<int> arr, const int* value) {
for (auto& x: arr) {
x += *value;
}
}#include <span>
)
Смысл этого кода очень простой: увеличить все числа в данном диапазоне на данную величину. Казалось бы, тут в цикле есть доступ по указателю, который имеет смысл вынести из тела (loop-invariant code motion). Но во что этот код переводит компилятор? lea rcx, [rdi+rsi*4]
Тело цикла располагается между метками
cmp rdi, rcx
je .L1
.L3:
mov eax, DWORD PTR [rdx]
add DWORD PTR [rdi], eax
add rdi, 4
cmp rdi, rcx
jne .L3
.L1:
ret.L1
и .L3
. Конкретно сейчас нас интересуют две инструкции:mov eax, DWORD PTR [rdx]
Выходит, что в регистре
add DWORD PTR [rdi], eaxrdx
располагается адрес, указатель value
, а в регистре rdi
— x
, адрес текущего элемента спана. На каждой итерации процессор загружает значение из памяти и потом складывает со значением в другом месте в памяти. Почему же?
Дело в том, что переданный указатель может указывать на сам элемент внутри спана. increment
может быть использована, например, так:void use_increment(std::span<int> a) {
И
increment(a, &a[1]);
}const
в составе типа указателя тут не панацея: он лишь запрещает модифицировать доступ через указатель, но не гарантирует, что значение, на которое он указывает, действительно не изменяется. Выходит, компилятор генерирует неэффективный код ради того, чтобы правильно работал случай, который программист навряд ли стал бы писать намеренно.
Замена указателя на ссылку ожидаемо не даёт никаких изменений. Вынос разыменовывания значения для инкремента в отдельную переменную перед циклом и использование её вместо указателя даёт желаемый результат в кодгене. Но что, если мы будем передавать указатели на другие типы?
Попробуем, например, short
:void increment(std::span<int> arr, const short* value) {
Что генерирует компилятор?
// тело без изменений
} lea rax, [rdi+rsi*4]
Ага, то есть доступ к
cmp rdi, rax
je .L1
movsx edx, WORD PTR [rdx]
.L3:
add DWORD PTR [rdi], edx
add rdi, 4
cmp rdi, rax
jne .L3
.L1:
retvalue
(с sign extension, разумеется) —movsx edx, WORD PTR [rdx]
— вынесен за пределы цикла! Так в чём же разница по сравнению с предыдущими примерами?
Вот тут как раз и вступает в силу правила strict aliasing (aka последний параграф в [basic.lval]): не смотря на то, что сформировать указатель на short
из указателя на int
можно, эти два типа отличаются, и получение доступа к значению одного типа через указатель на другой является неопределённым поведением. Так как в корректной программе на C++ неопределённого поведения не может произойти, компилятор использует этот факт, чтобы обосновать корректность выноса доступа к памяти из цикла.
Однако! У правил strict aliasing есть нюансы насчёт того, по указателям (на самом деле glvalue, но не суть) каких типов можно получать доступ к значениям других типов. В частности, unsigned
и signed
варианты того же типа не считаются существенно отличными, и потому при передаче const unsigned* value
компилятор оставляет доступ к value
в теле цикла.
BY Блог*

Share with your friend now:
group-telegram.com/dereference_pointer_there/4196