Comments 24
result = (keys[i] == target) ? values[i] : result;
Теперь values[i] читается на каждой итерации безусловно
С чего бы это? Как по вашему, упадет ли такой код: int c = b != 0 ? a/b : 0;
Неплохо бы ассемблерный код в примерах приводить - процессор всё таки не C исполняет ) У меня gcc -O3 на
result = (keys[i] == target) ? values[i] : result;
генерирует обычное условие (подозреваю, что в других примерах с тернарным оператором будет так же). И если таки сгенерировать branchless код (используя CMOV), values будет читаться только когда надо (скорее наоборот, исчезнут лишние спекулятивные чтения).
Компилятор может превратить это в CMOV
cmp keys[i], target
cmov result, values[i]Но это не гарантия и может сделать так
cmp keys[i], target
jne skip
mov result, values[i]
сделав обычное ветвление, это тоже зависит от включенных оптимизаций, целевой архитектуры, вероятности ветки по собранной эвристике и стоимости операций в этом месте. А еще это зависит от данных, и если VALUES[i] требует загрузки из памяти, то предпочитаетс JNE именно потому что CMOV в этом случае всё равно должен загрузить значение до выполнения, и латенси памяти попадает в хотпас независимо от условия. То есть CMOV с load-зависимостью иногда будет хуже, чем ветвление и компилятор это знает.
CMOV условно перемещает значение из источника в регистр, но источник должен быть УЖЕ готов до выполнения, сам CMOV не может условно отменить уже начатую загрузку из памяти, а только условно записывает результат.
CMOV в этом случае всё равно должен загрузить значение до выполнения
Не должен и не имеет права - потенциально загрузка может сгенерировать исключение (если адрес невалидный), при ложном условии этого быть не должно. CMOV сам инициирует загрузку из памяти после проверки условия.
Если явно указать компилятору более-менее современную архитектуру, генерируется векторный вариант с vpcmp/vpmaskmov - без бранчей, но опять же грузятся только элементы вектора, для которых условие сработало.
PS Таки прочитал документацию - вы правы, "CMOVcc loads data from its source operand into a temporary register unconditionally (regardless of the condition code and the status flags in the EFLAGS register)". И по тексту дальше генерация исключения от условия не зависит. Но тогда получается, что компилировать тернарный оператор в CMOV с аргументом в памяти нельзя - по семантике C/C++ выражение, соотвествующее невыполненному условию, не вычисляется, и никаких сторонних эффектов типа падения при разыменовении некорректного указателя быть не должно.
Процессор при branch prediction спекулятивно читает данные ещё до разрешения условия, но результат спекулятивного load при page fault не коммитится в retired состояние и исключение откладывается до момента, когда ветка "станет настоящей", что не противоречит обычному исполнению, неважно прочитали вы нулл из обычной ветки или спекулятивной.
Но есть другой нюанс, что компилятор может использовать CMOVcc только если "доказал" что оба оберанда валидны
Да, при спекулятивном выполнении проблем нет. Но для CMOV явно сказано, что исключение генерируется при невалидном адресе независимо от условия.
Для VPMASKMOV чётко сказано "Faults occur only due to mask-bit required memory accesses that caused the faults" - то есть если VPCMP не выставило бит, исключения точно не будет, так что компилятор спокойно может такое использовать. В принципе процессор может пытаться зачитать данные в любом случае, откладывая исключение как при спекулятивном выполнении - делает он так или сначала проверяет условие можно проверить микробенчмарками.
никаких сторонних эффектов типа падения при разыменовении некорректного указателя быть не должно.
Так в С++ разыменовывания некорректного указателя никогда не бывает (а там, где бывает – это как бы и не C++), поэтому компилятор спокойно тут пользуется CMOV-ом и вы недоуменно ловите исключение.
С производительностью примерно понятно, а что обычно выгоднее с точки зрения энергоэффективности? Медленное выполнение программы без предсказаний, или быстрое выполнение с некоторым количеством бесполезных операций?
Ну обычно мы тут в игрушки играем :) И новомодные E-коры ведут себя крайне коварно начиная тротлить в самый неподходящий момент, если вы про это. Поэтому основные потоки все забинжены на P-ядра, а на E-отдается всякая мелочь, вроде подгрузки текстур, распаковки банков и т.д. На консолях можно выставлять желаемую частоту ядра, если очень хочется, но я таких изысков пока не встречал. Задается максимум для двух первых ядер, а остальные под ОС живут.
Современные игровые движки действительно явно выставляют affinity для разных типов потоков с учётом гетерогенности? Интересно - согласен, что для стабильной работы это необходимо, но надо же спциально заморочиться...
Не, я чуть про другое. Вот нужно написать код, которые выполняет определенную задачу, израсходовав минимальное количество энергии из аккумулятора. Как мы будем его писать - с ветвлениями и предсказаниями, потратив лишнюю энергию на оказавшиеся ненужными инструкции и обращения к памяти? Или линейно, потратив лишнее время на ожидание данных из памяти?
По опыту на батарейку смотрят в самую последнюю очередь (я про игры), если вашей батарейки хватает на 15 минут игры (примерно столько сейчас длится одна сессия), то никто даже не будет про это думать.
В ИСП РАН раньше занимались такими вопросами по заказу от одного крупного производителя смартфонов на Android: в итоге там пришли к выводу, что единственный реально эффективный способ достичь экономии при потреблении энергии на мобильном устройстве на 10–25%,‑ это не модифицировать код на C/C++, а модифицировать gcc, так чтобы он генерировал энергоэффективный код. Стоит поискать не выкладывали ли они в публичный доступ свои наработки, но они точно использовались и статей они публиковали по теме очень много.
пойду думать над покупкой книги или курса, раз скидку дают
Эта и еще пара статей стала основой для одной из глав книги, целиком посвященой программированию без аллокаций, пулам, стековым и кастомным аллокаторам, другим подходам и тому, как всё это ведёт себя в реальном игровом движке под нагрузкой.
это просто всё обесценило, что было написано
мы смотрим предсказание/не предсказание в отрыве от реальности, и название статьи называется - Лишние вычисления, но как только мы ступаем на путь написания движка, мы вынуждены делать эти вычисления, чтобы были определенные состаяния, опять нету конкретики, нету рендер док, какой-то конкретики, полного состояния демки так сказать - всё оторвано, там на самом деле нужны все эти вычисления, и вы можете выбирать где будет кватернион, а где угол yaw-pitch, и это будет так углубляясь в тематику движков, и от этой точки захватывая все ускоряющие структуры(а их придётся захватывать потомучто это корень конструкции, даже грид в майнкрафте ускоряющий) становится понятно, что показывает NVIDIA на конференциях или крутые студии на тех же конференциях, а тут одна теория, подводя итог - важно всё со всем а не только пул или алокатор, важны состояния, да без выделений памяти ничего не будет работать это звучит как машине не нужен бензин
--
даже если подключаться к облаку вы всё равно сделаете аллокацию, тоесть аллокация будет в любом случае, наверно подразумевается что-то другое, но вобщем, состояние мира в любом случае будет подгружаться и в любом случае в память, и пока круче толстого клиента еще ничего нету
--
4гигабайта это было еще 2016 в одной известной игре, наверно только Quake, Arena или Dagerfall полетят менее чем на 4 гигабайтах
попробуйте Раст, С++ даже с модулями, это действительно уже догоняющий момент, Раст щас рвёт топ, там бойлерплейта поменьше
модули удобные, интерфейсы удобные, портянка на С++ даже на модулях близка к нечитаемости

Нужно, очень.
я просто считаю, когда рассказывают о таких конструкциях в геймдеве контекстные!, которые затрагивают что-то более менее конкретное, то чтобы было понятно о чем речь читателям надо иногда подтверждать контекст визуально
например полет частичек снизу показан
Скрытый текст

.section .text.math3d::geometry::particle::ParticleSystem::spawn_spark_burst,"ax",@progbits
.p2align 4
.type math3d::geometry::particle::ParticleSystem::spawn_spark_burst,@function
math3d::geometry::particle::ParticleSystem::spawn_spark_burst:
.cfi_startproc
.cfi_personality 155, DW.ref.rust_eh_personality
.cfi_lsda 27, .Lexception20
push rbp
.cfi_def_cfa_offset 16
push r15
.cfi_def_cfa_offset 24
push r14
.cfi_def_cfa_offset 32
push r13
.cfi_def_cfa_offset 40
push r12
.cfi_def_cfa_offset 48
push rbx
.cfi_def_cfa_offset 56
sub rsp, 72
.cfi_def_cfa_offset 128
.cfi_offset rbx, -56
.cfi_offset r12, -48
.cfi_offset r13, -40
.cfi_offset r14, -32
.cfi_offset r15, -24
.cfi_offset rbp, -16
mov qword ptr [rsp + 56], rsi
mov r14, qword ptr [rdi + 16]
mov qword ptr [rsp + 64], rdx
lea rax, [r14 + rdx]
cmp rax, qword ptr [rdi + 24]
ja .LBB59_22
mov r12, rdi
cmp byte ptr fs:[rand::rngs::thread::THREAD_RNG_KEY::{{constant}}::{{closure}}::__RUST_STD_INTERNAL_VAL@TPOFF+8], 1
jne .LBB59_3
mov rax, qword ptr fs:[0]
lea rax, [rax + rand::rngs::thread::THREAD_RNG_KEY::{{constant}}::{{closure}}::__RUST_STD_INTERNAL_VAL@TPOFF]
.LBB59_4:
mov rbx, qword ptr [rax]
inc qword ptr [rbx]
je .LBB59_5
lea r13, [rbx + 16]
lea rbp, [rbx + 288]
xor r15d, r15d
jmp .LBB59_10
.p2align 4
.LBB59_70:
mov rax, qword ptr [r12 + 8]
lea rcx, [r14 + 2*r14]
shl rcx, 4
movaps xmm0, xmmword ptr [rip + .LCPI59_10]
movups xmmword ptr [rax + rcx], xmm0
mov rsi, qword ptr [rsp + 56]
mov rdx, qword ptr [rsi]
mov qword ptr [rax + rcx + 16], rdx
mov edx, dword ptr [rsi + 8]
mov dword ptr [rax + rcx + 24], edx
movaps xmm0, xmmword ptr [rsp + 32]
movlps qword ptr [rax + rcx + 28], xmm0
movss xmm0, dword ptr [rsp + 8]
movss dword ptr [rax + rcx + 36], xmm0
movss xmm0, dword ptr [rsp + 16]
movss dword ptr [rax + rcx + 40], xmm0
movss xmm0, dword ptr [rsp + 12]
movss dword ptr [rax + rcx + 44], xmm0
inc r14
mov qword ptr [r12 + 16], r14
cmp r15, qword ptr [rsp + 64]
je .LBB59_7
.LBB59_10:
inc r15
mov rax, qword ptr [rbx + 272]
jmp .LBB59_11
.p2align 4
.LBB59_15:
mov rdi, rbp
mov rsi, r13
call rand::rngs::adapter::reseeding::ReseedingCore<R,Rsdr>::reseed_and_generate
.LBB59_16:
xor eax, eax
.LBB59_17:
mov ecx, dword ptr [rbx + 4*rax + 16]
inc rax
mov qword ptr [rbx + 272], rax
shr ecx, 9
or ecx, 1065353216
movd xmm1, ecx
movss xmm0, dword ptr [rip + .LCPI59_0]
addss xmm1, xmm0
addss xmm1, xmm1
addss xmm1, xmm0
movss xmm0, dword ptr [rip + .LCPI59_1]
movaps xmmword ptr [rsp + 32], xmm1
ucomiss xmm0, xmm1
ja .LBB59_18
.LBB59_11:
cmp rax, 64
jb .LBB59_17
mov rdx, qword ptr [rip + rand::rngs::adapter::reseeding::fork::RESEEDING_RNG_FORK_COUNTER]
mov rax, qword ptr [rbx + 344]
test rax, rax
jle .LBB59_15
cmp qword ptr [rbx + 352], rdx
js .LBB59_15
add rax, -256
mov qword ptr [rbx + 344], rax
mov rdi, rbp
mov rsi, r13
call rand_chacha::guts::refill_wide
jmp .LBB59_16
.p2align 4
.LBB59_33:
mov rdi, rbp
mov rsi, r13
call rand::rngs::adapter::reseeding::ReseedingCore<R,Rsdr>::reseed_and_generate
.LBB59_34:
xor eax, eax
.LBB59_35:
mov ecx, dword ptr [rbx + 4*rax + 16]
inc rax
mov qword ptr [rbx + 272], rax
shr ecx, 9
or ecx, 1065353216
movd xmm1, ecx
movss xmm0, dword ptr [rip + .LCPI59_0]
addss xmm1, xmm0
addss xmm1, xmm1
addss xmm1, xmm0
movss xmm0, dword ptr [rip + .LCPI59_1]
ucomiss xmm0, xmm1
ja .LBB59_36
.LBB59_18:
cmp rax, 64
jb .LBB59_35
mov rdx, qword ptr [rip + rand::rngs::adapter::reseeding::fork::RESEEDING_RNG_FORK_COUNTER]
mov rax, qword ptr [rbx + 344]
test rax, rax
jle .LBB59_33
cmp qword ptr [rbx + 352], rdx
js .LBB59_33
add rax, -256
mov qword ptr [rbx + 344], rax
mov rdi, rbp
mov rsi, r13
call rand_chacha::guts::refill_wide
jmp .LBB59_34
.p2align 4
.LBB59_36:
movaps xmmword ptr [rsp + 16], xmm1
jmp .LBB59_37
.p2align 4
.LBB59_41:
mov rdi, rbp
mov rsi, r13
call rand::rngs::adapter::reseeding::ReseedingCore<R,Rsdr>::reseed_and_generate
.LBB59_42:
xor eax, eax
.LBB59_43:
mov ecx, dword ptr [rbx + 4*rax + 16]
inc rax
mov qword ptr [rbx + 272], rax
shr ecx, 9
or ecx, 1065353216
movd xmm1, ecx
movss xmm0, dword ptr [rip + .LCPI59_0]
addss xmm1, xmm0
addss xmm1, xmm1
addss xmm1, xmm0
movss xmm0, dword ptr [rip + .LCPI59_1]
ucomiss xmm0, xmm1
ja .LBB59_44
.LBB59_37:
cmp rax, 64
jb .LBB59_43
mov rdx, qword ptr [rip + rand::rngs::adapter::reseeding::fork::RESEEDING_RNG_FORK_COUNTER]
mov rax, qword ptr [rbx + 344]
test rax, rax
jle .LBB59_41
cmp qword ptr [rbx + 352], rdx
js .LBB59_41
add rax, -256
mov qword ptr [rbx + 344], rax
mov rdi, rbp
mov rsi, r13
call rand_chacha::guts::refill_wide
jmp .LBB59_42
.p2align 4
.LBB59_44:
movss dword ptr [rsp + 8], xmm1
jmp .LBB59_45
.p2align 4
.LBB59_49:
mov rdi, rbp
mov rsi, r13
call rand::rngs::adapter::reseeding::ReseedingCore<R,Rsdr>::reseed_and_generate
.LBB59_50:
xor eax, eax
.LBB59_51:
mov ecx, dword ptr [rbx + 4*rax + 16]
inc rax
mov qword ptr [rbx + 272], rax
shr ecx, 9
or ecx, 1065353216
movd xmm2, ecx
addss xmm2, dword ptr [rip + .LCPI59_0]
mulss xmm2, dword ptr [rip + .LCPI59_2]
addss xmm2, dword ptr [rip + .LCPI59_3]
movss xmm0, dword ptr [rip + .LCPI59_4]
ucomiss xmm0, xmm2
ja .LBB59_52
.LBB59_45:
cmp rax, 64
jb .LBB59_51
mov rdx, qword ptr [rip + rand::rngs::adapter::reseeding::fork::RESEEDING_RNG_FORK_COUNTER]
mov rax, qword ptr [rbx + 344]
test rax, rax
jle .LBB59_49
cmp qword ptr [rbx + 352], rdx
js .LBB59_49
add rax, -256
mov qword ptr [rbx + 344], rax
mov rdi, rbp
mov rsi, r13
call rand_chacha::guts::refill_wide
jmp .LBB59_50
.p2align 4
.LBB59_52:
movaps xmm1, xmmword ptr [rsp + 32]
unpcklps xmm1, xmmword ptr [rsp + 16]
movss xmm0, dword ptr [rsp + 8]
mulss xmm0, xmm2
movss dword ptr [rsp + 8], xmm0
shufps xmm2, xmm2, 0
mulps xmm2, xmm1
movaps xmmword ptr [rsp + 32], xmm2
jmp .LBB59_53
.p2align 4
.LBB59_57:
mov rdi, rbp
mov rsi, r13
call rand::rngs::adapter::reseeding::ReseedingCore<R,Rsdr>::reseed_and_generate
.LBB59_58:
xor eax, eax
.LBB59_59:
mov ecx, dword ptr [rbx + 4*rax + 16]
inc rax
mov qword ptr [rbx + 272], rax
shr ecx, 9
or ecx, 1065353216
movd xmm1, ecx
addss xmm1, dword ptr [rip + .LCPI59_0]
addss xmm1, dword ptr [rip + .LCPI59_5]
movss xmm0, dword ptr [rip + .LCPI59_6]
ucomiss xmm0, xmm1
ja .LBB59_60
.LBB59_53:
cmp rax, 64
jb .LBB59_59
mov rdx, qword ptr [rip + rand::rngs::adapter::reseeding::fork::RESEEDING_RNG_FORK_COUNTER]
mov rax, qword ptr [rbx + 344]
test rax, rax
jle .LBB59_57
cmp qword ptr [rbx + 352], rdx
js .LBB59_57
add rax, -256
mov qword ptr [rbx + 344], rax
mov rdi, rbp
mov rsi, r13
call rand_chacha::guts::refill_wide
jmp .LBB59_58
.p2align 4
.LBB59_60:
movss dword ptr [rsp + 16], xmm1
jmp .LBB59_61
.p2align 4
.LBB59_65:
mov rdi, rbp
mov rsi, r13
call rand::rngs::adapter::reseeding::ReseedingCore<R,Rsdr>::reseed_and_generate
.LBB59_66:
xor eax, eax
.LBB59_67:
mov ecx, dword ptr [rbx + 4*rax + 16]
inc rax
mov qword ptr [rbx + 272], rax
shr ecx, 9
or ecx, 1065353216
movd xmm1, ecx
addss xmm1, dword ptr [rip + .LCPI59_0]
mulss xmm1, dword ptr [rip + .LCPI59_7]
addss xmm1, dword ptr [rip + .LCPI59_8]
movss xmm0, dword ptr [rip + .LCPI59_9]
ucomiss xmm0, xmm1
ja .LBB59_68
.LBB59_61:
cmp rax, 64
jb .LBB59_67
mov rdx, qword ptr [rip + rand::rngs::adapter::reseeding::fork::RESEEDING_RNG_FORK_COUNTER]
mov rax, qword ptr [rbx + 344]
test rax, rax
jle .LBB59_65
cmp qword ptr [rbx + 352], rdx
js .LBB59_65
add rax, -256
mov qword ptr [rbx + 344], rax
mov rdi, rbp
mov rsi, r13
call rand_chacha::guts::refill_wide
jmp .LBB59_66
.p2align 4
.LBB59_68:
movss dword ptr [rsp + 12], xmm1
cmp r14, qword ptr [r12]
jne .LBB59_70
mov rdi, r12
call alloc::raw_vec::RawVec<T,A>::grow_one
jmp .LBB59_70
.LBB59_7:
dec qword ptr [rbx]
je .LBB59_8
.LBB59_22:
add rsp, 72
.cfi_def_cfa_offset 56
pop rbx
.cfi_def_cfa_offset 48
pop r12
.cfi_def_cfa_offset 40
pop r13
.cfi_def_cfa_offset 32
pop r14
.cfi_def_cfa_offset 24
pop r15
.cfi_def_cfa_offset 16
pop rbp
.cfi_def_cfa_offset 8
ret
.LBB59_8:
.cfi_def_cfa_offset 128
mov rdi, rbx
add rsp, 72
.cfi_def_cfa_offset 56
pop rbx
.cfi_def_cfa_offset 48
pop r12
.cfi_def_cfa_offset 40
pop r13
.cfi_def_cfa_offset 32
pop r14
.cfi_def_cfa_offset 24
pop r15
.cfi_def_cfa_offset 16
pop rbp
.cfi_def_cfa_offset 8
jmp alloc::rc::Rc<T,A>::drop_slow
.LBB59_3:
.cfi_def_cfa_offset 128
mov rax, qword ptr fs:[0]
lea rdi, [rax + rand::rngs::thread::THREAD_RNG_KEY::{{constant}}::{{closure}}::__RUST_STD_INTERNAL_VAL@TPOFF]
call std::sys::thread_local::native::lazy::Storage<T,D>::get_or_init_slow
test rax, rax
jne .LBB59_4
call std::thread::local::panic_access_error
.LBB59_5:
ud2
jmp .LBB59_30
jmp .LBB59_30
jmp .LBB59_30
jmp .LBB59_30
jmp .LBB59_30
jmp .LBB59_30
.LBB59_30:
mov r14, rax
dec qword ptr [rbx]
jne .LBB59_32
mov rdi, rbx
call alloc::rc::Rc<T,A>::drop_slow
.LBB59_32:
mov rdi, r14
call _Unwind_Resume@PLTпонимаете? где тут нда в контексте бсп и бвх и полета частичек с физикой?
вы показали время, но хорошо бы показать плохой эффект визуально и хороший эффект визуально
вы показывали аллокаторы хорошо бы показать визульано как это отражается, вы показывали стрелу хорошо бы и её показать, иначе связь с геймдевом теряется и вы просто обсуждаете как кодить? тоесть не показываете то о чем говорите, тоесть нету подтверждения ваших взглядов в статьях вами самими, от этого не понятно как классифицировать что обсуждаем, почему это важно? потомучто С/С++ такой - любит конкретику, иначе это просто бесконечные формулировки кодьте так и всё
--
получается как раз в программировании и надо стараться четче показать пример, и четче сформулировать, о чем говорим, а просто время, которое все и так знают это не о чем просто
--
потом не понятна тогда мотивация? зачем мы вечно обсуждаем пулы - аллокации - время - для чего? вот когда вы обозревали движки вы показали только архитектуру, но в контексте сделать нету ничего, ни одного примера, получается читатель просто загуглит то что его интересует или спросит у друга в зале и он ему покажет и он запустит код и проверит - но тогда код уже будет цельный!
--
поэтому и можно хотябы в блендере показывать то о чем вы говорите или на движке каком-то, где там что стопорит по времени, есть даже проф движок g3d было бы желание показать только о чем вы пишите, к слову для прототипов у него есть бинд на луа
Боже, опять этот фонтан…
Лишние вычисления