1.84K subscribers
3.41K photos
134 videos
15 files
3.65K links
Блог со звёздочкой.

Много репостов, немножко программирования.

Небольшое прикольное комьюнити: @decltype_chat_ptr_t
Автор: @insert_reference_here
Download Telegram
Forwarded from ozkriff.games 🦀 (ozkriff🇺🇦)
# JetBrains Survey: How Rust and C/C++ Ecosystems Coexist?

Hi, folks! Please participate in our short survey:

https://jb.gg/rust_cpp

> We’d like to learn from Rust developers about their experience and best practices with C and C++ code in their Rust code base. Share your experience with us via the survey and get a chance to win a prize!
🤔1
Забавно, кажется, сейчас больше людей заметит удаление Блог*а, чем мою смерть
😱17🤔31😁1
Мам купи
😁72🔥1
#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 в теле цикла.
👍9🔥2🤯2
И ещё у этого правила есть совсем неожиданное исключение: доступ по указателю на char, unsigned char и std::byte. При передаче value по указателю на один из этих типов компилятор оставляет доступ в цикле.

Внимательные читатели могли заметить, что этот список не включает в себя signed char (и, кстати, в этом месте C++ отличается C, в котором алиаситься могут указатели любых вариантов char). Тем не менее, вариант increment с указателем на signed char последние версии и GCC (12.2), и clang (15.0.0) не могут скомпилировать с выносом доступа к value из цикла. Почему — не ясно. Наверное, это можно считать багом.

Поиграться со всеми упомянутыми вариантами: godbolt.org/z/8rr3hWTbf
(ссылка для расшаривания, к сожалению, потеряла все имена вкладок с кодом. Ну хоть имена вкладок с компиляторами оставила)
🔥4👍2
В СМЫСЛЕ УЖЕ НОЯ то есть ДЕКАБРЬ
👍5
Блог*
#prog #cpp #моё В C++ есть такая вещь, как strict aliasing. Если коротко, то это предположение компилятора о том, что доступы по указателям (и ссылкам) существенно разных типов не пересекаются между собой. Подробнее про это можно прочитать, например, тут…
В комментариях сказали, что не хватает демонстрации того, что Rust лучше. Что ж, исправляю.

Вот как выглядит идиоматичная функция с аналогичным функционалом (с поправкой на то, что, как и в коде на C++ в постах выше, идиоматично было бы принимать value вообще по значению):

fn increment(arr: &mut [i32], value: &i32) {
for x in arr {
*x += *value;
}
}

Rust 1.65.0 с флагом -C opt-level=1 выдаёт следующий код:

test rsi, rsi
sete al
je .LBB3_4
lea rsi, [rdi + 4*rsi]
mov ecx, dword ptr [rdx]
.LBB3_2:
not al
movzx eax, al
add dword ptr [rdi], ecx
and eax, 1
lea rdi, [rdi + 4*rax]
cmp rdi, rsi
sete al
je .LBB3_4
test rdi, rdi
jne .LBB3_2
.LBB3_4:
ret

Вывод несколько шумный (эти манипуляции с al и eax явно излишни), но самое главное: доступ к содержимому value осуществляется через

mov ecx, dword ptr [rdx]

— инструкцию вне цикла! И это вполне ожидаемо: в Rust есть более тонкие способы отслеживать aliasing, чем просто через типы. Тот факт, что набор чисел передаётся через ссылку &mut (и, в отличие от span в C++, это действительно именно ссылка, а не value-тип с ссылочной семантикой), уже позволяет считать все остальные доступы к памяти, как непересекающиеся. поскольку в Rust &mut — это уникальная ссылка. При кодогенерации компилятор может рассчитывать на этот факт, а при создании ссылок проверяет, что ссылка действительно уникальна.

(также можно было бы переписать эту функцию через доступы по индексам вместо прямого for x in arr, но, хотя тело цикла в этом случае чище, компилятор частично раскручивает цикл, что генерировало несколько больше ассемблерного кода, чем имеет смысл помещать в пост)

Возможно ли повторить на Rust семантику из C++? Да... С сигнатурой вида

fn increment(arr: *mut [UnsafeCell<MaybeUninit<i32>>], value: *mut MaybeUninit<i32>)

...Ладно, ладно, если что-то не столь страшное, а всё-таки более идиоматичное, но с возможностью пересечения доступа:

fn increment(arr: &[Cell<i32>], value: &Cell<i32>) {
for x in arr {
x.set(x.get() + value.get());
}
}

И действительно, если скомпилировать этот код с теми же флагами, то получится вот это:

test rsi, rsi
sete cl
je .LBB4_4
lea rax, [rdi + 4*rsi]
.LBB4_2:
not cl
movzx ecx, cl
mov esi, dword ptr [rdx]
add dword ptr [rdi], esi
and ecx, 1
lea rdi, [rdi + 4*rcx]
cmp rdi, rax
sete cl
je .LBB4_4
test rdi, rdi
jne .LBB4_2
.LBB4_4:
ret

Снова кучка лишних операций, но главное — это вот эти две операции в теле цикла:

mov esi, dword ptr [rdx]
add dword ptr [rdi], esi

То есть значение читается по ссылке каждый раз, как мы и (не) хотели.
👍12🔥1
Блог*
#prog #cpp #моё В C++ есть такая вещь, как strict aliasing. Если коротко, то это предположение компилятора о том, что доступы по указателям (и ссылкам) существенно разных типов не пересекаются между собой. Подробнее про это можно прочитать, например, тут…
Кстати, тот факт, что мы принимаем &[Cell<i32>], вовсе не означает, что нам нужно бегать по массивам и оборачивать элементы в Cell-ы. Можно воспользоваться двумя методами Cell:

fn into_cell_slice<T>(arr: &mut [T]) -> &[Cell<T>] {
    Cell::from_mut(arr).as_slice_of_cells()
}

Cell::from_mut работает за счёт того, что забирает переданную mut-ссылку и потому отбирает уникальный доступ до тех пор, пока возвращённая ссылка и её копии не будут дропнуты. Не смотря на то, что &Cell<T> даёт разделяемый доступ с возможностью поменять значение, это не создаёт проблем за счёт того, что изначально переданный доступ заведомо уникален, а доступ по &Cell<T> хоть и и не уникален, но заведомо находится в пределах одного потока: Cell<T> не реализует Sync, или, иными словами, ссылка на Cell не реализует Send, поэтому ссылку на Cell нельзя передать в другой поток. В рамках же одного потока доступы к одной локации в памяти никогда не происходят одновременно, и потому возможность создания гонок данных исключена.

Ну а Cell::as_slice_of_cells имеет очень простое оправдание: если мы имеем де-факто мутабельный доступ к какому-то значению, то мы можем разделить его на мутабельные доступы к его отдельным компонентам. При этом как-то подменить слайс целиком, чтобы поменять его длину, и инвалидировать ссылки на его элементы нельзя: все соответствующие методы или требуют уникальную ссылку, или работают только с Sized типами, которым слайс не является.
3👍3🔥2
Forwarded from Jem
😁16😐1
Forwarded from ЕНОТ ИЗДАЕТ
Ваша последняя книжная покупка? 👀
8
а ведь реальна блин
(кто скажет что боян тому зуб откушу)
11🤡2😁1
Forwarded from ТГ Шевченка
👍132
Forwarded from 🇺🇦 Go for two :)
The [adventofcode] just started

Happy hacking!
https://adventofcode.com/2022/day/1
🤔3👍2