首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >为什么新的安置要比直接的任务快得多?

为什么新的安置要比直接的任务快得多?
EN

Stack Overflow用户
提问于 2015-08-26 09:53:55
回答 3查看 950关注 0票数 22

我最近发现,使用新的职位比完成16项任务更快:

考虑下面的代码段(c++11):

代码语言:javascript
复制
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;
    };
};

用法:

代码语言:javascript
复制
Matrix m;
//modify m.data

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

在我的机器上,Identity1()比第二个函数快25倍。现在我想知道为什么会有这么大的差别?

我还试了第三个:

代码语言:javascript
复制
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循环‘测试,但也有外部分析测试:

剖析方法1:(众所周知的循环测试)

代码语言:javascript
复制
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);

方法2:

代码语言:javascript
复制
$ 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

编译和机器信息

用:g++ --std=c++11 -O3 -g -pg -Wall编译 -pg不是问题所在。在没有使用这个标志的情况下,测量方法1得到了相同的时间差。

代码语言:javascript
复制
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
EN

回答 3

Stack Overflow用户

发布于 2015-08-27 22:54:03

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

使用定时代码,两个版本都编译到完全相同的asm:一个空循环。您发布的代码从不使用m,因此可以对其进行优化。所发生的就是循环计数器的加载/存储。(这是因为您使用了volatile int,告诉gcc变量存储在内存映射的I/O空间中,所以所有出现在源中的读/写都必须实际出现在asm中。MSVC对volatile关键字有不同的含义,这超出了标准的要求。)

看看在asm会议上。下面是您的代码,以及它转化为的asm:

代码语言:javascript
复制
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
代码语言:javascript
复制
  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。(在这种情况下,它使用AVX32B加载/存储从4x32B的数据复制。没有什么比字节更聪明的了--移动寄存器将1.0移动到不同的位置:/)

如果gcc更聪明,它会做一个16B的两个零的存储,而不是两个movsd。也许它是假设不对齐的,并且如果存储对齐,那么背线或页行在非对齐存储上的缺点要比保存存储的好处要糟糕得多。

所以不管你用那个代码计时什么,都不是你的函数。除非他们中的一个做了Identity,而另一个没有。不管怎么说,从你的循环计数器中丢失volatile,这是完全愚蠢的。只需看看空循环中的额外负载/存储就可以了。

票数 2
EN

Stack Overflow用户

发布于 2015-08-27 01:33:50

我敢打赌,如果手动复制const数组,您将获得相同的性能:

代码语言:javascript
复制
static constexpr double identity_data[16] = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 };

void Identity3()
{
    std::copy(std::begin(identity_data), std::end(identity_data), data);
}
票数 1
EN

Stack Overflow用户

发布于 2015-08-27 08:18:07

对这个问题感兴趣的是,我发现了一篇关于SSE指令的非常好的博客文章,讨论了movq和movsd的性能:

http://www.gamedev.net/blog/615/entry-2250281-demystifying-sse-move-instructions/

由于第二组指令movsd/movsq不执行零扩展名,您可能会认为它们比那些需要额外填充零movd/movq.的指令要快一些。但是,这些指令可能会导致对先前指令的错误依赖,因为处理器不知道您是否打算使用最终没有擦除的额外数据。在无序执行期间,这可能会导致流水线上的停顿,而移动指令则等待必须写入该寄存器的任何先前指令。如果您实际上不需要这种依赖,那么您就不必要地在应用程序中引入了减速。

因此,更复杂的指令解码会与管道起作用,其中其他指令必须假定依赖关系。解码本身可能也同样快。

组件页面上尝试一些东西时,我也惊讶于一个简单的memset转化为内联程序集是多么糟糕,而我所期望的只是一个简单的rep stosq或一个展开的版本。

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/32223377

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档