Telegram Group & Telegram Channel
#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], eax

Выходит, что в регистре rdx располагается адрес, указатель value, а в регистре rdix, адрес текущего элемента спана. На каждой итерации процессор загружает значение из памяти и потом складывает со значением в другом месте в памяти. Почему же?

Дело в том, что переданный указатель может указывать на сам элемент внутри спана. 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:
ret

Ага, то есть доступ к value (с sign extension, разумеется) —

movsx edx, WORD PTR [rdx]

— вынесен за пределы цикла! Так в чём же разница по сравнению с предыдущими примерами?

Вот тут как раз и вступает в силу правила strict aliasing (aka последний параграф в [basic.lval]): не смотря на то, что сформировать указатель на short из указателя на int можно, эти два типа отличаются, и получение доступа к значению одного типа через указатель на другой является неопределённым поведением. Так как в корректной программе на C++ неопределённого поведения не может произойти, компилятор использует этот факт, чтобы обосновать корректность выноса доступа к памяти из цикла.

Однако! У правил strict aliasing есть нюансы насчёт того, по указателям (на самом деле glvalue, но не суть) каких типов можно получать доступ к значениям других типов. В частности, unsigned и signed варианты того же типа не считаются существенно отличными, и потому при передаче const unsigned* value компилятор оставляет доступ к value в теле цикла.



group-telegram.com/dereference_pointer_there/4196
Create:
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], eax

Выходит, что в регистре rdx располагается адрес, указатель value, а в регистре rdix, адрес текущего элемента спана. На каждой итерации процессор загружает значение из памяти и потом складывает со значением в другом месте в памяти. Почему же?

Дело в том, что переданный указатель может указывать на сам элемент внутри спана. 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:
ret

Ага, то есть доступ к value (с 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

View MORE
Open in Telegram


Telegram | DID YOU KNOW?

Date: |

In the United States, Telegram's lower public profile has helped it mostly avoid high level scrutiny from Congress, but it has not gone unnoticed. "The inflation fire was already hot and now with war-driven inflation added to the mix, it will grow even hotter, setting off a scramble by the world’s central banks to pull back their stimulus earlier than expected," Chris Rupkey, chief economist at FWDBONDS, wrote in an email. "A spike in inflation rates has preceded economic recessions historically and this time prices have soared to levels that once again pose a threat to growth." Telegram has gained a reputation as the “secure” communications app in the post-Soviet states, but whenever you make choices about your digital security, it’s important to start by asking yourself, “What exactly am I securing? And who am I securing it from?” These questions should inform your decisions about whether you are using the right tool or platform for your digital security needs. Telegram is certainly not the most secure messaging app on the market right now. Its security model requires users to place a great deal of trust in Telegram’s ability to protect user data. For some users, this may be good enough for now. For others, it may be wiser to move to a different platform for certain kinds of high-risk communications. Despite Telegram's origins, its approach to users' security has privacy advocates worried. Again, in contrast to Facebook, Google and Twitter, Telegram's founder Pavel Durov runs his company in relative secrecy from Dubai.
from us


Telegram Блог*
FROM American