Булевское умножение в С++?
Рассмотрим следующее:
inline unsigned int f1(const unsigned int i, const bool b) {return b ? i : 0;}
inline unsigned int f2(const unsigned int i, const bool b) {return b*i;}
Синтаксис f2
более компактен, но выполняют ли стандартные гарантии, что f1
и f2
являются строго эквивалентными?
Кроме того, если я хочу, чтобы компилятор оптимизировал это выражение, если b
и i
известны во время компиляции, какую версию я должен предпочесть?
Ответы
Ответ 1
Ну, да, оба эквивалентны. bool
является интегральным типом, и true
гарантированно преобразуется в 1
в целочисленном контексте, а false
гарантированно преобразуется в 0
.
(Обратное также верно, т.е. не равные нулю целые значения гарантируют преобразование в true
в булевом контексте, в то время как нулевые целые значения гарантированно преобразуются в false
в булевом контексте.)
Поскольку вы работаете с неподписанными типами, вы можете легко найти другие, возможно, на основе бит-хака, но совершенно портативные реализации одной и той же вещи, например
i & -(unsigned) b
хотя достойный компилятор должен иметь возможность выбирать лучшую реализацию отдельно для любой из ваших версий.
P.S. Хотя, к моему большому удивлению, GCC 4.1.2 скомпилировал все три варианта практически буквально, т.е. Использовал инструкцию машинного умножения в варианте на основе умножения. Это было достаточно разумно, чтобы использовать инструкцию cmovne
для варианта ?:
, чтобы сделать ее ветвящейся, что вполне возможно сделало ее наиболее эффективной.
Ответ 2
Да. Можно с уверенностью предположить, что true
- 1
, а false
- 0
при использовании в выражениях, как вы это делаете, и гарантируется:
С++ 11, интегральные рекламные акции, 4.5:
rvalue типа bool может быть преобразовано в rvalue типа int, с false становится нулевым и истинным становится единым.
Ответ 3
Компилятор будет использовать неявное преобразование, чтобы сделать unsigned int
от b
, поэтому да, это должно работать. Вы пропускаете проверку состояния простым умножением. Какой из них эффективнее/быстрее? Не знаю. Хороший компилятор, скорее всего, оптимизирует обе версии, которые я бы предположил.
Ответ 4
FWIW, следующий код
inline unsigned int f1(const unsigned int i, const bool b) {return b ? i : 0;}
inline unsigned int f2(const unsigned int i, const bool b) {return b*i;}
int main()
{
volatile unsigned int i = f1(42, true);
volatile unsigned int j = f2(42, true);
}
скомпилированный с помощью gcc -O2, создает эту сборку:
.file "test.cpp"
.def ___main; .scl 2; .type 32; .endef
.section .text.startup,"x"
.p2align 2,,3
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
LFB2:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $16, %esp
call ___main
movl $42, 8(%esp) // i
movl $42, 12(%esp) // j
xorl %eax, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE2:
Там не так много осталось от f1
или f2
, как вы можете видеть.
Что касается стандарта С++, компилятору разрешено делать что-либо в отношении оптимизации, если оно не изменяет наблюдаемое поведение (как правило).