【问题标题】:Clang sizeof("literal") optimizationClang sizeof("literal") 优化
【发布时间】:2016-02-07 08:07:49
【问题描述】:

体验过 C++,我试图了解 sizeofstrlen 对于字符串文字的性能差异。

这是我的小基准代码:

#include <iostream>
#include <cstring>

#define LOOP_COUNT 1000000000

unsigned long long rdtscl(void)
{
    unsigned int lo, hi;
    __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
    return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 );
}

int main()
{
    unsigned long long before = rdtscl();
    size_t ret;
    for (int i = 0; i < LOOP_COUNT; i++)
        ret = strlen("abcd");
    unsigned long long after = rdtscl();
    std::cout << "Strlen " << (after - before) << " ret=" << ret <<     std::endl;

    before = rdtscl();
    for (int i = 0; i < LOOP_COUNT; i++)
        ret = sizeof("abcd");
    after = rdtscl();
    std::cout << "Sizeof " << (after - before) << " ret=" << ret << std::endl;
}

clang++编译,得到如下结果:

clang++ -O3 -Wall -o sizeof_vs_strlen sizeof_vs_strlen.cpp
./sizeof_vs_strlen

Strlen 36 ret=4
Sizeof 62092396 ret=5

g++:

g++ -O3 -Wall -o sizeof_vs_strlen sizeof_vs_strlen.cpp 
./sizeof_vs_strlen

Strlen 30 ret=4
Sizeof 30 ret=5

我强烈怀疑g++ 确实使用sizeof 优化了循环,而clang++ 没有。 这个结果是一个已知问题吗?

编辑:

clang++ 为带有sizeof 的循环生成的程序集:

rdtsc  
mov    %edx,%r14d
shl    $0x20,%r14
mov    $0x3b9aca01,%ecx
xchg   %ax,%ax
add    $0xffffffed,%ecx // 0x400ad0
jne    0x400ad0 <main+192>
mov    %eax,%eax
or     %rax,%r14
rdtsc  

还有g++

rdtsc  
mov    %edx,%esi
mov    %eax,%ecx
rdtsc

我不明白为什么 clang++ 做 {add, jne} 循环,它似乎没用。它是一个错误吗?

有关信息:

g++ (GCC) 5.1.0
clang version 3.6.2 (tags/RELEASE_362/final)

编辑2: 它可能是clang 中的一个错误。 我开了一个bug report

【问题讨论】:

  • 要做的第一件事是检查您正在测试的每个语句的程序集......我希望任何编译器都将这两个优化为硬编码值
  • 1/ 读取程序集(或提供它) 2/ 如果您尝试测量 strlen 的性能,您做错了,因为您的基准测试允许编译器进行计算以进行编译-时间。
  • 您可以自己查看优化是否发生,将 -S 标志添加到 clang 或 gcc,您可以查看程序集输出并比较 clang 与 gcc。奇怪的是,如果你省略 1. 循环,clang 也会优化 2. 你的循环。
  • 感谢您的 cmets。我添加了生成的汇编代码。 Pascal Cuoq > 我知道,我从 O0 开始,但后来发现 O3 这个奇怪的结果。
  • 顺便说一句:你使用unsigned long long,这在c++11之前的标准中是没有的,但是你的编译命令缺少std=c++11

标签: c++ gcc clang compiler-optimization


【解决方案1】:

我将其称为 clang 中的错误。

它实际上是优化sizeof 本身,而不是循环。

为了使代码更清晰,我将std::cout 更改为printf,然后您将获得以下LLVM-IR 代码用于main:

; Function Attrs: nounwind uwtable
define i32 @main() #0 {
entry:
  %0 = tail call { i32, i32 } asm sideeffect "rdtsc", "={ax},={dx},~{dirflag},~{fpsr},~{flags}"() #2, !srcloc !1
  %asmresult1.i = extractvalue { i32, i32 } %0, 1
  %conv2.i = zext i32 %asmresult1.i to i64
  %shl.i = shl nuw i64 %conv2.i, 32
  %asmresult.i = extractvalue { i32, i32 } %0, 0
  %conv.i = zext i32 %asmresult.i to i64
  %or.i = or i64 %shl.i, %conv.i
  %1 = tail call { i32, i32 } asm sideeffect "rdtsc", "={ax},={dx},~{dirflag},~{fpsr},~{flags}"() #2, !srcloc !1
  %asmresult.i.25 = extractvalue { i32, i32 } %1, 0
  %asmresult1.i.26 = extractvalue { i32, i32 } %1, 1
  %conv.i.27 = zext i32 %asmresult.i.25 to i64
  %conv2.i.28 = zext i32 %asmresult1.i.26 to i64
  %shl.i.29 = shl nuw i64 %conv2.i.28, 32
  %or.i.30 = or i64 %shl.i.29, %conv.i.27
  %sub = sub i64 %or.i.30, %or.i
  %call2 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([21 x i8], [21 x i8]* @.str, i64 0, i64 0), i64 %sub, i64 4)
  %2 = tail call { i32, i32 } asm sideeffect "rdtsc", "={ax},={dx},~{dirflag},~{fpsr},~{flags}"() #2, !srcloc !1
  %asmresult1.i.32 = extractvalue { i32, i32 } %2, 1
  %conv2.i.34 = zext i32 %asmresult1.i.32 to i64
  %shl.i.35 = shl nuw i64 %conv2.i.34, 32
  br label %for.cond.5

for.cond.5:                                       ; preds = %for.cond.5, %entry
  %i4.0 = phi i32 [ 0, %entry ], [ %inc10.18, %for.cond.5 ]
  %inc10.18 = add nsw i32 %i4.0, 19
  %exitcond.18 = icmp eq i32 %inc10.18, 1000000001
  br i1 %exitcond.18, label %for.cond.cleanup.7, label %for.cond.5

for.cond.cleanup.7:                               ; preds = %for.cond.5
  %asmresult.i.31 = extractvalue { i32, i32 } %2, 0
  %conv.i.33 = zext i32 %asmresult.i.31 to i64
  %or.i.36 = or i64 %shl.i.35, %conv.i.33
  %3 = tail call { i32, i32 } asm sideeffect "rdtsc", "={ax},={dx},~{dirflag},~{fpsr},~{flags}"() #2, !srcloc !1
  %asmresult.i.37 = extractvalue { i32, i32 } %3, 0
  %asmresult1.i.38 = extractvalue { i32, i32 } %3, 1
  %conv.i.39 = zext i32 %asmresult.i.37 to i64
  %conv2.i.40 = zext i32 %asmresult1.i.38 to i64
  %shl.i.41 = shl nuw i64 %conv2.i.40, 32
  %or.i.42 = or i64 %shl.i.41, %conv.i.39
  %sub13 = sub i64 %or.i.42, %or.i.36
  %call14 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([21 x i8], [21 x i8]* @.str, i64 0, i64 0), i64 %sub13, i64 5)
  ret i32 0
}

如您所见,对 printf 的调用使用来自 sizeof 的常量 5,而 for.cond.5: 启动空循环: 一个“phi”节点(根据我们来自的位置选择 i 的“新”值 - 循环前 -> 0,循环内 -> %inc10.18) 增量 如果%inc10.18 不是 100000001,则返回一个条件分支。

我对 clang 和 LLVM 的了解还不够,无法解释为什么没有优化掉那个空循环。但肯定不是sizeof 需要时间,因为循环内没有sizeof

值得注意的是,sizeof 在编译时始终是一个常量,除了将常量值加载到寄存器之外,它永远不会“花费时间”。

【讨论】:

  • 我同意你的看法,clang 留下了一个空循环。我会等着看“clang专家”是否回复,否则我会将您的答案标记为已接受。谢谢。
  • 我会说这绝对是一个错误 - 我正在开发(作为一种爱好)基于 LLVM 的 Pascal 编译器(这与 Clang 使用的东西相同),离开中间有sizeof 时循环,但调用strlen 时不循环。我会看看能否从“知情者”那里得到更好的答案,但我正在上班的路上。
  • 有趣的是,将sizeof 移出循环,(现在为空的)循环确实会被删除。
  • 似乎确认了这个bug。我今天会尝试提交错误报告。
  • 所以在一个电子邮件线程中,Richard Smith 确认这绝对是一个错误,它是由相同的变量 ret 用于两个循环引起的。我刚刚通过添加ret2 并为其分配sizeof 来确认这一点,然后打印它,现在两个“循环”都需要相同的时间。
【解决方案2】:

不同的是,sizeof() 不是函数调用。 sizeof() “返回”的值在编译时是已知的。 同时, strlen 是函数调用(显然是在运行时执行的)并且根本没有优化。它在字符串中寻找 '\0',它甚至不知道它是动态分配的字符串还是字符串字面量。

因此,对于字符串文字,sizeof 应该总是更快。


我不是专家,但您的结果可能通过调度算法或时间变量溢出来解释。

【讨论】:

  • 请参阅我对clang++ 生成的汇编代码的编辑。这既不是由于取消调度(多次测试,固定在一个独特的核心上,精度为 -20)也不是由于溢出(rdtscl 返回一个unsigned long long 并且程序运行不到一秒)。
  • 在 3.5GHZ 处理器上,假设 rdtsc 在每次启动时重置为 0,OP 必须在其系统上运行 appx. 167 years 才会溢出。
猜你喜欢
  • 1970-01-01
  • 2013-03-10
  • 2022-10-15
  • 2020-12-14
  • 2020-07-19
  • 2018-11-01
  • 2020-01-06
  • 1970-01-01
相关资源
最近更新 更多