dbbd
dbbd

Reputation: 894

c++ compile-time assertion

I'm using a macro that might be dangerous:

#define REMAINDER(v, size) ((v) & (size -1))

obviously it assumes that size is a power of 2.

I would like to make sure size is indeed a power of 2, but at compile time. (testing at run time is easy, but is NOT what I want).

A sufficient test for me would be that size is always a constant (never a variable).

I would use BOOST_STATIC_ASSERT, but I cannot figure out how to use it for what I need.

Upvotes: 3

Views: 2428

Answers (5)

First things first: that micro optimization is not needed. Any decent compiler with optimizations enabled will convert a % b into that construct when b is a compile time constant that is actually a power of 2.

Then on the particular assert, you can use the same construct to assert it [*]:

BOOST_STATIC_ASSERT( !(size & (size-1)) );

[*] Note that as Matthieu M points out this only works if size is an unsigned type. And that should be asserted --the lesser requirement that the argument is non-negative cannot be asserted at compile time:

BOOST_STATIC_ASSERT( (X(0)-1) > X(0) ); // where X is the type of the argument

EDIT after last comment:

You are missing the point here. For a static assertion macro to work, the size must be a compile time constant. If it is a compile time constant then just assert when the constant is defined which is also the best place, as it will serve as documentation, and will point to the precise point of code that needs modification:

template <typename N>
class hash_map {
public:
   const std::size_t size = N;
   BOOST_STATIC_ASSERT( !(size & (size-1) ) ); // N must be power of 2 for fast %
   //...
};

At the same time that asserting that the invariant is held at compile time is important for efficiency, obscuring the code is not: Just leave the modulo operation in place, as the compiler will optimize:

std::size_t hash_map::index_of( std::size_t hash ) const {
   return hash % size;
}

Because size is a compile time constant, and it is a power of two (you asserted that before) the optimizer will translate % into the optimized operation, while the code is still readable by humans that need to maintain it.

Upvotes: 6

Matthieu M.
Matthieu M.

Reputation: 300369

Peephole Optimization: Optimization performed on a very small set of instructions

This includes:

  • Constant Folding: eg, here size - 1 would be evaluated during compilation if size is constant
  • Strength Reduction: which consists in replacing slow operations by faster ones... when the result is equivalent (obviously)

Now, let's see an example of this using the LLVM backend optimizer:

// C
int iremainder(int i) { return i % 4; }

unsigned uremainder(unsigned u) { return u % 4; }

// LLVM IR
define i32 @iremainder(i32 %i) nounwind readnone {
entry:
  %0 = srem i32 %i, 4                             ; <i32> [#uses=1]
  ret i32 %0
}

define i32 @uremainder(i32 %u) nounwind readnone {
entry:
  %0 = and i32 %u, 3                              ; <i32> [#uses=1]
  ret i32 %0
}

Let's analyze, would you ?

  • u / 4 yields and i32 %u, 3 (which translated back in C gives u & 3)
  • i / 4 yields srem i32 %i, 4 (which translated back in C gives i % 4)

What a smart compiler! It didn't forget that performing bitwise operations on signed integers would not yield the result I wanted (whenever the integer is negative, and branches are more expensive than divisions).

Morale: This kind of optimization is near useless, and you even got it wrong.

Upvotes: 1

Erik
Erik

Reputation: 91320

assert on:

size && (size & (size - 1) == 0)

EDIT: Explanation.

If size is a power of two, only one bit is set. Subtracting 1 will yield a value where all bits up to the original is set. ANDing these gives 0.

If size is not a power of two, at least two bits are set. Subtracting 1 will yield a value where all bits up to the original's lowest set bit is set. ANDing these will not yield 0, since the second and later (from right) bits are still set.

1000 & 0111 == 0000
1100 & 1011 == 1000

Upvotes: 3

Mark B
Mark B

Reputation: 96311

EDIT: Unless you have profiling proof that this is a bottleneck in your code AND that your compiler isn't optimizing thing like (v % 8) appropriately, just write the obvious code.

Otherwise since you're dealing with integers here can you use a template instead of a macro? Then you should be able to static assert inside the template so it'll flag when it's instantiated incorrectly.

For example something like:

template <int size>
int remainder(int v)
{
    BOOST_STATIC_ASSERT(!(size & (size - 1)));

    return v & (size -1);
};

Upvotes: 5

Lightness Races in Orbit
Lightness Races in Orbit

Reputation: 385395

// Only use this if size is a power of 2
#define REMAINDER(v, size) ((v) & (size -1))

Don't treat your users like idiots. Document your functions and macros, and move on.


Alternatively if you really must molly-coddle people, use a template:

template <size_t SIZE>
size_t remainder(size_t v) {
   // Perform whatever assertions you like on SIZE here

   return v & (SIZE - 1);
}

Note that I've had to make some assumptions about the type of input and output.

Upvotes: 2

Related Questions