0

I am looking forward to check if a 3 bits number is prime using both logical and relational operators.The number is represented using 3 variables with bits 7-1 set to 0 and only the bit on position 0 being the actual data. Suppose we have:

unsigned char x3, x2, x1;

One can assume that a prime number is function f which outputs 1 if the number is prime, 0 otherwise.

How would one solve this using bit-wise operations (logical operators) as optimal as possible? One can assume that a minimum conjunctive/disjunctive form can be extracted from a K.V. diagram of the truth table.

How would one solve this using relational operators?

Which one would be faster?

Some useful data:

CDF: (~x2 & X1) | (X0 & X2)
CCF: (X1 | X2) & (X0 | ~X2)
CyberFox
  • 342
  • 3
  • 16
  • 1
    `0`, `1`, `2`, ..., and `7` are the 3-bit numbers. Of these `2`, `3`, `5`, and `7` are prime. Am I missing something? – pmg Mar 19 '19 at 21:22
  • 1
    I think OP just wants to know which way is faster, which would depend on CPU architecture. But yeah, otherwise just take input as an integer and `return (num != 4) && (num != 6)`... – Reticulated Spline Mar 19 '19 at 21:24
  • 1
    @ReticulatedSpline `0` and `1` are not primes. You need to check for them too. – klutt Mar 19 '19 at 21:34
  • 2
    I think `x3 ? x1 : x2` should do the trick (assuming `x3` represents the most-significant of the 3 bits). – Mike Holt Mar 19 '19 at 21:39
  • 1
    Easy to do `bool primeArray[8] = { false, ... };` and then look it up with no bit twiddling needed. Clear and simple. – Weather Vane Mar 19 '19 at 21:44

3 Answers3

3

Bitwise

I think the best you can do here is (x3 & x1) | (~x3 & x2). In boolean algebra, this would be expressed as AC + (!A)B.* None of the usual rules for simplifying boolean algebra expressions would seem to apply here, and several online boolean algebra expression simplifiers seem to agree.

* (the second A would normally be written with a bar over it, but I don't know how to do that in markdown).

So you'd get something like this (using uchar as shorthand for unsigned char):

uchar f_bitwise(uchar x3, uchar x2, uchar x1) 
{
   return (x3 & x1) | (~x3 & x2);
}

The assembly produced by this (with -O0 and discarding the function call overhead), looks like this:

movzx   eax, BYTE PTR [rbp-4]  # move x3 into register eax
and     al, BYTE PTR [rbp-12]  # bitwise AND the lower half of eax with x1
mov     ecx, eax               # store the result in ecx
cmp     BYTE PTR [rbp-4], 0    # compare x3 with 0
sete    al                     # set lower half of eax to 1 if x3 was equal to 0
mov     edx, eax               # store the result in edx (this now equals ~x3)
movzx   eax, BYTE PTR [rbp-8]  # move x2 into eax
and     eax, edx               # bitwise AND ~x3 (in edx) with x2 (in eax)
or      eax, ecx               # finally, bitwise OR eax and ecx

The result is stored in eax.

Logical

Looking at the bits of values 0-7, and trying to discern an easy pattern to key off of, you notice that for values 0-3, the number is prime if and only if x2 is 1. Likewise, for values 4-7, the number is prime if and only if x1 is 1. This observation yields a simple expression: x3 ? x1 : x2.

I have no proof that this is the shortest possible expression using logical operators, so if someone has a shorter version, by all means post it in a comment. However, it does seem unlikely that there's a shorter version, given that this is essentially a single logical operator, as you can see if you expand the ternary operator into a proper if/else:

uchar f_logical(uchar x3, uchar x2, uchar x1) 
{
   if (x3 != 0) 
      return x1;
   else
      return x2;
}

The assembly produced by this is as follows (again with -O0 and not counting the function call overhead):

cmp     BYTE PTR [rbp-4], 0      # compare x3 with 0
je      .L2                      # if equal, jump to label L2
movzx   eax, BYTE PTR [rbp-12]   # move x1 into register eax
jmp     .L4                      # jump to label L4 (i.e., return from function)
.L2: 
movzx   eax, BYTE PTR [rbp-8]    # move x2 into register eax
.L4:
# Function return. Result is once again stored in eax.

I haven't tested the performance of either of these functions, but just from looking at the assembly, it seems almost certain that f_logical would run faster than f_bitwise. It uses significantly fewer instructions, and although fewer instructions doesn't always equate to faster, none of these instructions seem like they would be particularly expensive in terms of CPU cycles.

If you cancel out the instructions that both functions have in common and compare what's left, you get:

f_logical: je, jmp

f_bitwise: and (2), mov (2), sete, or

As for why the logical version is shorter, I think the answer is branching. With only bitwise operations and no branching, you have to account for all possibilities in a single expression.

For instance, in (x3 & x1) | (~x3 & x2), it would be nice to get rid of the ~x3 on the right hand side, given that you already know x3 is zero there, given that the right hand side represents the test for values 0-3. But the computer has no way of knowing this, and you can't factor it out into a simpler expression.

With the ability to branch, you can split the problem into two sub-problems using a single comparison operator. Again, this works because for values 0-3, the x2 bit is essentially an "is prime" bit, and for values 4-7, the x1 bit is an "is prime" bit.

Also, alinsoar is correct that a lookup table would be faster, but only if the value isn't split into individual bits. With the bit values in separate variables, you either have to reconstruct the number using something like x3<<2 | x2<<1 | x1, or you have to define your lookup table as a 3D array, in which case the compiler generates a bunch of extra instructions to do the address arithmetic necessary to index a 3D array.

Mike Holt
  • 4,452
  • 1
  • 17
  • 24
1

Because there are not many inputs, you can define a pre-computed table PRIME that has 1 on the positions of prime numbers and 0 for the rest.

For example, PRIME(0,1,1) = 1 while PRIME(1,0,1)=0, i.e. PRIME(3)=true, PRIME(6)=false.

alinsoar
  • 15,386
  • 4
  • 57
  • 74
1

The shorter solution is:

int isPrime(unsigned char x3, unsigned char x2, unsigned char x1) {
  return x1 | (x2 & ~x3);
}
  • x1 is to match all odd numbers. In the interval [1..7] they are all prime.
  • (x2 & ~x3) is to match the value 2 (in fact it match 2 and 3).

With Compiler Explorer you can compare the code generated by various compilers on various architectures. Example with gcc x86_64 vs ARM64 : https://godbolt.org/z/JwtES4

Note: For a small function like that a #define will be faster and shorted than a function call.

#define isPrime(x3,x2,x1) ((x1) | ((x2) & ~(x3)))
Jim
  • 11
  • 1
  • 1
    Except [1 is not prime](https://primes.utm.edu/notes/faq/one.html). See also [here](http://mathworld.wolfram.com/PrimeNumber.html). From the latter article: "The number 1 is a special case which is considered neither prime nor composite. Although the number 1 used to be considered a prime, it requires special treatment in so many definitions and applications involving primes greater than or equal to 2 that it is usually placed into a class of its own." – Mike Holt Mar 19 '19 at 23:04
  • 1
    I think the best you can do with bitwise ops is `(x3 & x1) | (~x3 & x2)`. In boolean algebra, this would be of the form `AC + (!A)B` (don't know how to do an A with a bar over it), and I'm not aware of any rule that would allow simplifying it further. I could be wrong about this, but all of the online boolean algebra expression simplifiers I've tried seem to agree it can't be simplified. – Mike Holt Mar 19 '19 at 23:19