forcing usage of bitwise and instead of boolean an

2019-09-19 11:26发布

问题:

In C++, a bool is guaranteed to be 0 or 1

C++ (§4.5/4):
An rvalue of type bool can be converted to an rvalue of type int, with 
false becoming zero and true becoming one.

Consider the following function and what g++5.2 generates with -O3

int foo(bool a, bool b)
{
    if(a&b) return 3;
    else return 5;
}


0000000000000000 <_Z3foobb>:
   0:   40 84 ff                test   %dil,%dil
   3:   74 13                   je     18 <_Z3foobb+0x18>
   5:   40 84 f6                test   %sil,%sil
   8:   b8 03 00 00 00          mov    $0x3,%eax
   d:   74 09                   je     18 <_Z3foobb+0x18>
   f:   f3 c3                   repz retq 
   11:  0f 1f 80 00 00 00 00    nopl   0x0(%rax)
   18:  b8 05 00 00 00          mov    $0x5,%eax
   1d:  c3                      retq   

As seen, above, it is generating two test instructions which indicates that it is still treating the if as a if(a&&b) instead of a bitwise and.

Even if I first explicitly convert the two bools to two chars , it still generates the same output as above.

Since I know that the two operands a and b can only have 0/1 as values, is there some way to get gcc to generate just one test instruction. This is indeed what it does if the function takes two ints instead of two bools.

回答1:

With &, some compiler already produces different asm without jump:

clang 3.6 (rc2):

foo(bool, bool):                               # @foo(bool, bool)
    testb   %sil, %dil
    sete    %al
    movzbl  %al, %eax
    leal    3(%rax,%rax), %eax
    retq

A hack, in your case is to use * which has the same true-table for true/false

int foo(bool a, bool b)
{
    if (a * b) return 3;
    else return 5;
}

Which produces:

foo(bool, bool):
    movzbl  %sil, %esi  # b, b
    movzbl  %dil, %edi  # a, tmp70
    imull   %esi, %edi  # b, tmp70
    cmpl    $1, %edi    #, tmp70
    sbbl    %eax, %eax  # D.1960
    andl    $2, %eax    #, D.1960
    addl    $3, %eax    #, D.1960
    ret

Demo



回答2:

It is doing a bitwise-AND. That is the definition of what & does, and the compiler correctly implements the observable behaviour for bitwise-AND.

The operands of binary & undergo the integer promotions, so a and b are converted to int. The definition of this conversion is:

  • true is converted to 1
  • false is converted to 0

Doing a bitwise-AND, here is an exhaustive list of cases:

  • 1 & 1 == 1
  • 1 & 0 == 0
  • 0 & 1 == 0
  • 0 & 0 == 0

As you can see, this is the same list of cases are for a boolean AND. Therefore you cannot distinguish the operation merely by looking at the assembly output.

To confirm this, here is an example showing that g++ generates the exact same assembly for if (a&b) as for if (a&&b) .

Remember that C++ is defined in terms of an abstract machine, and the output of the compiler may be anything at all so long as it produces the same observable behaviour as the abstract machine would.


You seem to be objecting to the part of the code associated with the conversion of bool to int. Your compiler apparently has implemented bool such that any non-zero bit pattern is considered true.

So, extra steps are necessary. Performing test %dil, %sil would give the wrong result in the case that one of the bools had bit pattern 000...0010 and the other had 000....0001, for example.

As user657267 points out in comments, the compiler has decided to test non-zero on one argument, and then test non-zero on the other argument, which it is easy to show that it fulfils the requirements of the defined behaviour for & along with the integer promotions as described above.

There's nothing you can do about this. You could submit a g++ patch so that it only ever generates bit pattern 000...0001 for a true bool and therefore it could use test %dil, %sil here but I doubt it would be accepted.


It would be possible to write code that reads the representation of a and b and performs & on the lowest byte. However this would be dangerous; the compiler might have decided to send some other bit pattern than you are expecting because it knows that those are acceptable bit patterns for true.

An alternative solution might be to change your function to accept unsigned int arguments instead of bool. Of course this would introduce the possibility of new bugs, e.g. if you called the function with 1ull << 40 it would truncate 1ull << 40 to 0 and therefore behave the same as passing in false. You can't eat your cake and have it; in this scenario you'd have to be very careful every time you call your function. (Maybe writing a wrapper class for int would help!)



回答3:

I feel like this may not quite be what you are looking for but normally arithmetic and's can only be exicuted if the objects being tested are of type int so a simple cast may fix that but I would assume a cast could generate extra unwanted instructions that you don't want as well.

int foo(bool a, bool b)
{ 
    if((int)a&(int)b) return 3;
    else return 5;
}