为什么安置新员工比直接分配工作快得多?
Why is a placement new much faster than a direct assignment?
我最近发现使用放置new比执行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循环'测试,但也有外部分析测试:
分析方法1:(众所周知的循环测试)
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: %ldn", elapsed);
方法2:$ 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中获得了相同的时差
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
关键字有不同的含义,这超出了标准的规定。
看一下godbolt上的asm。下面是您的代码,以及它转换成的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存储两个0,而不是两个movsd
。也许它是假设未对齐的,而在未对齐的存储上使用缓存线或页行分割的缺点比在对齐时保存存储的优点要糟糕得多。
所以无论你对代码进行了什么计时,它都不是你的函数。除非其中一个做了Identity
,而另一个没有。无论哪种方式,从循环计数器中删除volatile
,这完全是愚蠢的。只要看看空循环中额外的加载/存储就知道了。
我敢打赌,如果您手动memcopy const-expr数组,您将获得相同的性能:
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);
}
对这个问题很感兴趣,我找到了一篇非常好的关于SSE指令的博客文章,在这里讨论了movq和movsd的性能:
http://www.gamedev.net/blog/615/entry - 2250281 -揭秘sse -移动instructions/
由于第二组指令[
movsd
/movsq
]不做零扩展,你可能会认为他们会比那些不得不这样做的人快一点做额外的零填充[movd
/movq
]。然而,这些指令可以引入对先前指令的错误依赖,因为处理器不知道您是否打算使用额外的数据最后没有擦除。在乱序执行期间,这可能导致在移动指令等待任何指令时,在管道中停止必须写入该寄存器的先前指令。如果你实际上并不需要这种依赖关系,您不必要地引入了应用程序运行速度变慢。
因此,更复杂的指令解码与管道合作,其中其他指令必须承担依赖关系。解码本身可能也一样快。
在汇编页面上尝试了一些东西,我也惊讶于一个简单的memset
转换成内联汇编是多么糟糕,而我所期望的只是一个简单的rep stosq
或其展开版本。
- 为什么它在不分配内存的情况下工作正常
- C++:检查动态取消分配是否正常工作
- 虚函数如何工作,分配后新的返回类型会发生什么?
- 为什么必须动态分配扩展数组才能使此功能正常工作C++
- 将正常函数的工作分配给多个线程是否安全
- 在不工作的情况下为数组分配指针,但反过来也可以
- scanf() 语句中"%*[^n]"的格式字符串指示什么?分配抑制器 (*) 和否定扫描集 ([^) 如何协同工作?
- 使用 std::map 的递归堆栈分配如何工作?
- 不工作 复制分配运算符
- 返回对象如何与分配运算符一起工作
- 如果不允许我分配 rvalues 来引用为什么以下代码片段有效,这在内部如何工作?
- 向量::在C 中分配工作
- 将工作与固定数量的螺纹之间的工作和pthread之间的分配
- 在打印出动态分配的数组中的前两个数字时遇到问题,其他数字工作正常
- OpenCL:动态内存分配,是使用空闲工作项更好还是同时写入更好
- C++动态内存分配未按预期工作
- 如果堆分配的对象被销毁并且指针被重新分配,Qt的信号和插槽系统会工作吗?
- 为什么安置新员工比直接分配工作快得多?
- 将c实现的静态分配工作区转换为c++
- 不同类型的指针之间的分配工作,我不知道为什么