【问题标题】:c++ compile-time assertionc++ 编译时断言
【发布时间】:2011-03-03 16:06:34
【问题描述】:

我正在使用一个可能很危险的宏:

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

显然它假定大小是 2 的幂。

我想确保 size 确实是 2 的幂,但在编译时。 (运行时测试很简单,但不是我想要的)。

对我来说,一个充分的测试是 size 始终是一个常数(绝不是变量)。

我会使用BOOST_STATIC_ASSERT,但我不知道如何使用它来满足我的需要。

【问题讨论】:

  • 这里有另一个建议:停止这些微不足道的微优化工作并将其留给编译器。无论如何它都可以做得更好,而且你的代码不会无缘无故地被丑陋的东西填满。即使您的编译器是 1970 年的并且没有对此进行优化,它仍然可能不需要/对性能绝对没有明显影响!
  • 这样做的目的是什么?当然,任何体面的优化器都会优化涉及二次幂的操作!?!
  • 带有 -O3 的 GCC 将 var % constant 优化到恰好如果 constant 是 2 的幂。这是一个众所周知且易于实现的优化,所以我假设其他编译器也会这样做。
  • 无论如何(关于我的评论,@delnan's 和@Nim's),虽然手动微优化可能并不重要,但确保数字是 2 的幂(这样编译器实际上可以执行优化本身)是一个有效的目标。也就是说,虽然我会删除该宏,但在编译时确保 size 是 2 的幂是一个明智的想法。
  • LLVM 后端正确处理(对于 4),甚至更好的是,如果 v (出于某种原因)签名,则不会生成错误代码。

标签: c++ boost assertions


【解决方案1】:

首先要做的是:不需要微优化。当b 是一个实际上是2 次幂的编译时间常数时,任何启用了优化的体面的编译器都会将a % b 转换为该结构。

然后在特定的断言上,您可以使用相同的构造来断言它[*]:

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

[*] 请注意,正如 Matthieu M 指出的那样,这仅在 size 是无符号类型时才有效。这应该被断言——在编译时不能断言参数为非负的较小要求:

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

最后评论后编辑:

你在这里错过了重点。要使静态断言宏起作用,size 必须是编译时间常数。如果它是一个编译时常量,那么只需在定义常量时断言,这也是最好的地方,因为它将用作文档,并将指向需要修改的代码的精确点:

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 %
   //...
};

同时断言在编译时保持不变量对效率很重要,但隐藏代码并不是:只需将模运算留在原处,因为编译器会优化:

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

因为size 是一个编译时间常数,它是2 的幂(你之前断言过)优化器会将% 转换为优化的操作,而代码仍然可供需要维护的人阅读它。

【讨论】:

  • 这将无法处理 size==0。
  • 请同时断言操作数的参数是无符号类型。
  • 这就是我想要的。所以我的宏应该是这样的: do { BOOST_STATIC_ASSERT( !(size & (size-1)) ); ((v) & (大小 -1)) } 而(0);但是我不能在右值表达式中使用它。 grrr。
  • @dbbd:您有点忽略了这一点,编译器需要静态断言来生成快速模运算,但您不需要手动实现它,将该任务留给编译器。另外,每次使用前不需要断言,它是编译时常量,只在调用的地方断言!
【解决方案2】:

编辑:除非您有分析证明这是您的代码中的瓶颈,并且您的编译器没有适当地优化 (v % 8) 之类的东西,否则只需编写明显的代码。

否则,既然您在这里处理整数,您可以使用模板而不是宏吗?然后你应该能够在模板中进行静态断言,以便在错误实例化时进行标记。

例如:

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

    return v & (size -1);
};

【讨论】:

  • 我同意编写明显的代码,但同时编译器需要断言将明显的翻译成高效模运算。
  • int... arg 执行按位优化!
【解决方案3】:

断言:

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

编辑:解释。

如果 size 是 2 的幂,则只设置一位。减去 1 将产生一个值,其中设置了原始位之前的所有位。对这些进行与运算得到 0。

如果大小不是 2 的幂,则至少设置两个位。减去 1 将产生一个值,其中设置了原始最低设置位之前的所有位。对这些进行与运算不会产生 0,因为仍然设置了第二个和后面的(从右起)位。

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

【讨论】:

  • 查看大卫的答案以获得更小的公式:)
  • 一个较小的公式,因为它不处理 size==0
  • @Erik: 0 &amp; (0xFFFFFFFF) 产生0,这否定了产生true,有什么问题?
  • @Matthieu:0 不是 2 的幂,0 将是 OP 约束内的无效大小。
  • @Erik: 啊对...模 0 可能会造成严重破坏。不过,我更喜欢对这种情况进行显式检查(即 0 的一个断言,因为在模数中遇到一个奇怪的值,因此逻辑是错误的,而一个断言是 2 个问题的幂,因为它只是滥用此功能)。
【解决方案4】:
// Only use this if size is a power of 2
#define REMAINDER(v, size) ((v) & (size -1))

不要像白痴一样对待您的用户。记录您的函数和宏,然后继续。


或者,如果您真的必须哄人,请使用模板:

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

   return v & (SIZE - 1);
}

请注意,我必须对输入和输出的类型做出一些假设。

【讨论】:

  • +1,我已恢复平价,原始答案或编辑后的答案都没有问题...
  • 我没有投反对票,但让编译器执行你自己的规则是 优秀 的做法,而不是“溺爱”。接口的编译器强制执行可能是 C++ 存在的最大原因,并且总是优于文档或信任的开发人员。
  • @tenfour:不适合像这样的微小的微优化。你好,代码膨胀!
  • 如果您看到y=8;...x=REMAINDER(y,3); 并且后来 8 必须更改为 9,您不太可能去阅读 REMAINDER 的 cmets,并且您会遇到运行时错误。我们都同意的真正解决方案是他应该只使用operator % 而忘记宏(不要盲目地溺爱编译器!)。在这种情况下,您需要一个函数,该函数要求参数必须是 2 的幂,最好让编译器强制执行,而不是默默地允许运行时错误。 “记录你的功能并继续前进”是危险的;我们应该支持编译器解释而不是人工解释。
  • 即使“将REMAINDER 重命名为REMAINDER_FOR_POWERS_OF_2”也比“只发表评论”更好。
【解决方案5】:

Peephole Optimization:在非常小的指令集上执行优化

这包括:

  • Constant Folding:例如,如果size 为常量,则此处的size - 1 将在编译期间进行评估
  • Strength Reduction: 包括用较快的操作替换慢速操作...当结果相等时(显然)

现在,让我们看一个使用 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
}

我们来分析一下好吗?

  • u / 4 产生 and i32 %u, 3(在 C 中翻译回 u &amp; 3
  • i / 4 产生 srem i32 %i, 4(在 C 中翻译回 i % 4

多么智能编译器!它没有忘记对有符号整数执行按位运算不会产生我想要的结果(只要整数为负,并且分支比除法更昂贵)。

士气:这种优化几乎没用,你甚至弄错了。

【讨论】:

  • 所有假设使用优化的答案由于该假设而无效。不优化代码的原因有很多,但这是另一个话题。
  • +1 带来了我们大多数人(至少我)错过的部分测试。不过,我认为这里有两个不同的问题:首先在编译时断言可以使用快速操作,然后让编译器为您优化。不管编译器多么聪明,如果你把常数改成不是 2 的幂,那么优化器将什么也做不了。
  • @dbbd:我不确定你所说的assume the use of optimization 是什么意思。如果您想说您没有使用编译器优化,那么您可以获得的最快优化就是在编译器调用中添加适当的标志!
  • @David:我同意这并不能解决编译检查问题。但我的观点不仅仅是试图超越编译器(就像许多过早的优化倾向于那样)是徒劳的。无论如何,编译检查已经在其他答案中得到解决:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-22
  • 2014-08-11
  • 1970-01-01
相关资源
最近更新 更多