c++ - 为什么新的展示位置比直接分配要快得多?

标签 c++ performance c++11 gcc x86-64

我最近发现使用新的展示位置比执行 16 次作业要快:
考虑以下代码(c++11):

class Matrix
{
public:
    double data[16];

    Matrix() : data{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }
    {
    };

    void Identity1()
    {
        new (this) Matrix();
    };

    void Identity2()
    {
        data[0]  = 1.0; data[1]  = 0.0; data[2]  = 0.0; data[3]  = 0.0;
        data[4]  = 0.0; data[5]  = 1.0; data[6]  = 0.0; data[7]  = 0.0;
        data[8]  = 0.0; data[9]  = 0.0; data[10] = 1.0; data[11] = 0.0;
        data[12] = 0.0; data[13] = 0.0; data[14] = 0.0; data[15] = 1.0;
    };
};

用法:

Matrix m;
//modify m.data

m.Identity1(); //~25 times faster
m.Identity2();

在我的机器上 Identity1() 比第二个函数快大约 25 倍。现在我很好奇为什么会有这么大的差异?

我还尝试了第三个:

void Identity3()
{
    memset(data, 0, sizeof(double) * 16);
    data[0] = 1.0;
    data[5] = 1.0;
    data[10] = 1.0;
    data[15] = 1.0;
};

但这比 Identity2() 还要慢,我无法想象为什么。


分析信息

我已经做了几个分析测试,看看它是否是一个与分析相关的问题,所以有默认的“for循环”测试,还有外部分析测试:

Profiling方法一:(众所周知的for循环测试)

struct timespec ts1;
struct timespec ts2;

clock_gettime(CLOCK_MONOTONIC, &ts1);

for (volatile int i = 0; i < 10000000; i++)
    m.Identity(); //use 1 or 2 here

clock_gettime(CLOCK_MONOTONIC, &ts2);

int64_t start = (int64_t)ts1.tv_sec * 1000000000 + (int64_t)ts1.tv_nsec;
int64_t elapsed = ((int64_t)ts2.tv_sec * 1000000000 + (int64_t)ts2.tv_nsec) - start;

if (elapsed < 0)
    elapsed += (int64_t)0x100000 * 1000000000;

printf("elapsed nanos: %ld\n", elapsed);

方法二:

$ valgrind --tool=callgrind ./testcase

$ # for better overview:
$ python2 gprof2dot.py -f callgrind.out.22028 -e 0.0 -n 0.0 | dot -Tpng -o tree.png

装配信息

作为用户 T.C.在评论中说明,这可能会有所帮助:

http://goo.gl/LC0RdG


编译和机器信息

Compiled with: g++ --std=c++11 -O3 -g -pg -Wall

-pg is not the issue. Got the same time-difference in measurement method 1 without using this flag.

Machine info (lscpu):

Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                8
On-line CPU(s) list:   0-7
Thread(s) per core:    2
Core(s) per socket:    4
Socket(s):             1
NUMA node(s):          1
Vendor ID:             GenuineIntel
CPU family:            6
Model:                 58
Model name:            Intel(R) Core(TM) i7-3612QM CPU @ 2.10GHz
Stepping:              9
CPU MHz:               2889.878
CPU max MHz:           3100.0000
CPU min MHz:           1200.0000
BogoMIPS:              4192.97
Virtualization:        VT-x
L1d cache:             32K
L1i cache:             32K
L2 cache:              256K
L3 cache:              6144K
NUMA node0 CPU(s):     0-7

最佳答案

无论您测量的 25 倍时差是多少,这实际上并不是两个 Identity() 实现之间的差异。

使用您的计时代码,两个版本都编译为完全相同的 asm:一个空循环。您发布的代码从不使用 m,因此它被优化掉了。所发生的只是循环计数器的加载/存储。 (发生这种情况是因为您使用 volatile int 来告诉 gcc 该变量存储在内存映射的 I/O 空间中,因此出现在源代码中的所有读/写都必须实际出现在 asm 中.MSVC 对 volatile 关键字的含义不同,超出了标准所说的范围。)

看看at the asm on godbolt .这是您的代码,以及它变成的 asm:

for (volatile int i = 0; i < 10000000; i++)
    m.Identity1();
// same output for gcc 4.8.2 through gcc 5.2.0, with -O3

# some setup before this loop:  mov $0, 8(%rsp)  then test if it reads back as 0
.L16:
    movl    8(%rsp), %eax
    addl    $1, %eax
    movl    %eax, 8(%rsp)
    movl    8(%rsp), %eax
    cmpl    $9999999, %eax
    jle .L16

  for (volatile int i = 0; i < 10000000; i++)
    m.Identity2();

# some setup before this loop:  mov $0, 12(%rsp)  then test if it reads back as 0
.L15:
    movl    12(%rsp), %eax
    addl    $1, %eax
    movl    %eax, 12(%rsp)
    movl    12(%rsp), %eax
    cmpl    $9999999, %eax
    jle .L15

如您所见,没有人调用任一版本的 Identity() 函数。

有趣的是,在 Identity1 的 asm 中,它使用整数 movq 分配零,而 Identity2 仅使用标量 FP 移动.这可能与使用 0.0 与 0 有关,也可能是由于就地 new 与简单赋值有关。

无论哪种方式,gcc 5.2.0 都不会矢量化 Identity 函数,除非您使用 -march=native。 (在这种情况下,它使用 AVX 32B 加载/存储从 4x 32B 的数据中复制。没有什么比字节移位寄存器以将 1.0 移动到不同位置更聪明的了:/)

如果 gcc 更智能,它会存储 16B 的两个零,而不是两个 movsd。也许它假设未对齐,并且未对齐存储上的缓存行或页面行拆分的缺点比保存对齐的存储 insn 的优点要糟糕得多。


因此,无论您使用该代码计时,它都不是您的函数。除非他们中的一个做了 Identity,而另一个没有。无论哪种方式,从循环计数器中丢失 volatile ,这完全是愚蠢的。因为它,只需查看空循环中的额外加载/存储。

关于c++ - 为什么新的展示位置比直接分配要快得多?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32223377/

相关文章:

java - 有没有类似于Android Market上的bug报告系统的bug报告库?

mysql - 删除 "Using filesort"以进行简单的 MySQL 查询

c++ - 将 MSVC++ .lib 文件与 mingw 一起使用。名称修改

c++ - 使用 makefile 在最终可执行文件之前创建库

performance - 读取/索引大索引时出现 SOLR 内存不足错误

c# - 在控制台中显示时格式化 IEnumerable<double>

c++ - 使用 const 对象 move 语义

c++ - Uniform_real 不接受 numeric_limits::lowest()

c++ - 在函数中编码两个 vector

java - 如何使用 JNI 在 C++ 中访问 java 类的元素