Ответ 1
void reset_if_true(void*& ptr, bool cond)
{
if (cond)
ptr = nullptr;
}
Наивное решение, несомненно, будет самым быстрым в большинстве случаев. Хотя у этого есть ветвь, которая может быть медленной на современных конвейерных процессорах, это только медленно, если ветвь неверно предсказана. Поскольку предсказатели ветвления очень хороши в наши дни, если значение cond
крайне непредсказуемо, вероятно, что простая условная ветка является самым быстрым способом записи кода.
И если это не так, хороший компилятор должен это знать и иметь возможность оптимизировать код для чего-то лучшего, учитывая целевую архитектуру. Что идет в gnasher729 point: просто напишите код простым способом и оставьте оптимизацию в руках оптимизатора.
Хотя это хороший совет в целом, иногда он забирается слишком далеко. Если вам действительно нужна скорость этого кода, вам нужно проверить и посмотреть, что на самом деле делает компилятор. Проверьте код объекта, который он генерирует, и убедитесь, что он является разумным и что код функции становится вложенным.
Такое исследование может быть довольно показательным. Например, рассмотрим x86-64, где ветки могут быть довольно дорогими в случаях, когда отклонение от ветвления (это действительно единственный момент, когда это интересный вопрос, поэтому допустим, что cond
полностью непредсказуем). Почти все компиляторы собираются создать для наивной реализации следующее:
reset_if_true(void*&, bool):
test sil, sil ; test 'cond'
je CondIsFalse
mov QWORD PTR [rdi], 0 ; set 'ptr' to nullptr, and fall through
CondIsFalse:
ret
Это примерно такой же сложный код, как вы могли себе представить. Но если вы положите предиктор ветки в патологический случай, это может оказаться медленнее, чем использование условного перемещения:
reset_if_true(void*&, bool):
xor eax, eax ; pre-zero the register RAX
test sil, sil ; test 'cond'
cmove rax, QWORD PTR [rdi] ; if 'cond' is false, set the register RAX to 'ptr'
mov QWORD PTR [rdi], rax ; set 'ptr' to the value in the register RAX
ret ; (which is either 'ptr' or 0)
Условные ходы имеют относительно высокую задержку, поэтому они значительно медленнее, чем хорошо предсказанная ветвь, но они могут быть быстрее, чем полностью непредсказуемая ветвь. Вы ожидали бы, что компилятор узнает об этом при ориентации на архитектуру x86, но не имеет (по крайней мере, на этом простом примере) знания о предсказуемости cond
. Он предполагает простой случай, что предсказание ветвления будет на вашей стороне и генерирует код A вместо кода B.
Если вы решите, что хотите побудить компилятор генерировать нераспределенный код из-за непредсказуемого состояния, вы можете попробовать следующее:
void reset_if_true_alt(void*& ptr, bool cond)
{
ptr = (cond) ? nullptr : ptr;
}
Это преуспевает в том, чтобы убедить современные версии Clang генерировать ветвящийся код B, но это полная пессимизация в GCC и MSVC. Если вы не проверили сгенерированную сборку, вы бы этого не знали. Если вы хотите заставить GCC и MSVC генерировать нераспаковывающийся код, вам придется больше работать. Например, вы можете использовать вариант, опубликованный в вопросе:
void reset_if_true(void*& ptr, bool cond)
{
void* p[] = { ptr, nullptr };
ptr = p[cond];
}
При таргетинге на x86 все компиляторы генерируют для этого нераспространяемый код, но это не особо красивый код. Фактически, ни один из них не создает условные ходы. Вместо этого вы получаете множественный доступ к памяти, чтобы построить массив:
reset_if_true_alt(void*&, bool):
mov rax, QWORD PTR [rdi]
movzx esi, sil
mov QWORD PTR [rsp-16], 0
mov QWORD PTR [rsp-24], rax
mov rax, QWORD PTR [rsp-24+rsi*8]
mov QWORD PTR [rdi], rax
ret
Уродливый и, вероятно, очень неэффективный. Я бы предсказывал, что он дает условную версию перехода за свои деньги даже в том случае, если ветвь неверно предсказана. Конечно, вам нужно было бы проверить это, конечно, но это, вероятно, не очень хороший выбор.
Если вы все еще отчаянно пытались устранить ветвь на MSVC или GCC, вам нужно было бы сделать что-то более уродливое, включающее переинтерпретирование битов указателя и их скручивание. Что-то вроде:
void reset_if_true_alt(void*& ptr, bool cond)
{
std::uintptr_t p = reinterpret_cast<std::uintptr_t&>(ptr);
p &= -(!cond);
ptr = reinterpret_cast<void*>(p);
}
Это даст вам следующее:
reset_if_true_alt(void*&, bool):
xor eax, eax
test sil, sil
sete al
neg eax
cdqe
and QWORD PTR [rdi], rax
ret
Опять же, здесь у нас есть больше инструкций, чем простая ветка, но, по крайней мере, они являются относительно низкозатратными инструкциями. Тест на реалистичные данные скажет вам, стоит ли компромисс. И дайте вам обоснование, которое вам нужно поставить в комментарий, если вы собираетесь на самом деле зарегистрировать код, как это.
Как только я спустился по крошечной кроличьей дыре, я смог заставить MSVC и GCC использовать условные инструкции перемещения. По-видимому, они не делали эту оптимизацию, потому что мы имели дело с указателем:
void reset_if_true_alt(void*& ptr, bool cond)
{
std::uintptr_t p = reinterpret_cast<std::uintptr_t&>(ptr);
ptr = reinterpret_cast<void*>(cond ? 0 : p);
}
reset_if_true_alt(void*&, bool):
mov rax, QWORD PTR [rdi]
xor edx, edx
test sil, sil
cmovne rax, rdx
mov QWORD PTR [rdi], rax
ret
Учитывая задержку CMOVNE и аналогичное количество инструкций, я не уверен, действительно ли это будет быстрее предыдущей версии. Тест, который вы использовали, скажет вам, было ли это.
Точно так же, если мы свернем условие, мы сохраняем один доступ к памяти:
void reset_if_true_alt(void*& ptr, bool cond)
{
std::uintptr_t c = (cond ? 0 : -1);
reinterpret_cast<std::uintptr_t&>(ptr) &= c;
}
reset_if_true_alt(void*&, bool):
xor esi, 1
movzx esi, sil
neg rsi
and QWORD PTR [rdi], rsi
ret
(что GCC. MSVC делает что-то немного другое, предпочитая свою характерную последовательность команд neg
, sbb
, neg
и dec
, но эти два являются морально эквивалентными. Clang преобразует его в тот же условный перейдите, что мы увидели, что он генерирует выше.) Это может быть лучший код, если нам нужно избегать ветвей, учитывая, что он генерирует разумный вывод для всех тестируемых компиляторов при сохранении (некоторой степени) удобочитаемости в исходном коде.