好的,问题标题有点糟糕,但我真的不知道如何更好地表达这一点。
我遇到的问题是,给定std::vector<T>
与T*
+ size_t count
我的编译器(Visual Studio 2005 / VC ++ 8)实际上会在循环指针时生成更糟糕的代码而不是在矢量上循环。
也就是说,我有一个包含向量的测试结构,另一个包含指针+计数。现在,在编写语义完全相同的循环结构时,带有std :: vector的版本显着(也就是说> 10%)比带指针的版本快。
您将在下面找到代码以及生成的程序集。如果有人能解释这里发生了什么,那就太好了。
如果查看程序集,可以注意原始指针版本如何生成稍多的指令。如果有人能解释这些版本在汇编级别上的语义差异,那将是一个非常好的答案。
并且请不要回答告诉我我不应该关心,过早优化,所有邪恶的根源等等。在这个特定情况下我做关心,无论如何我认为这是一个相当有趣的难题! : - )
编译器设置:
代码如下:
stdafx.h中
// Disable secure STL stuff!
#define _SECURE_SCL 0
#define _SECURE_SCL_THROWS 0
#include <iostream>
#include <iomanip>
#include <vector>
#include <mmsystem.h>
头文件
// loop1.h
typedef int PodType;
const size_t container_size = 3;
extern volatile size_t g_read_size;
void side_effect();
struct RawX {
PodType* pData;
PodType wCount;
RawX()
: pData(NULL)
, wCount(0)
{ }
~RawX() {
delete[] pData;
pData = NULL;
wCount = 0;
}
void Resize(PodType n) {
delete[] pData;
wCount = n;
pData = new PodType[wCount];
}
private:
RawX(RawX const&);
RawX& operator=(RawX const&);
};
struct VecX {
std::vector<PodType> vData;
};
void raw_loop(const int n, RawX* obj);
void raw_iterator_loop(const int n, RawX* obj);
void vector_loop(const int n, VecX* obj);
void vector_iterator_loop(const int n, VecX* obj);
实施文件
// loop1.cpp
void raw_loop(const int n, RawX* obj)
{
for(int i=0; i!=n; ++i) {
side_effect();
for(int j=0, e=obj->wCount; j!=e; ++j) {
g_read_size = obj->pData[j];
side_effect();
}
side_effect();
}
}
void raw_iterator_loop(const int n, RawX* obj)
{
for(int i=0; i!=n; ++i) {
side_effect();
for(PodType *j=obj->pData, *e=obj->pData+size_t(obj->wCount); j!=e; ++j) {
g_read_size = *j;
side_effect();
}
side_effect();
}
}
void vector_loop(const int n, VecX* obj)
{
for(int i=0; i!=n; ++i) {
side_effect();
for(size_t j=0, e=obj->vData.size(); j!=e; ++j) {
g_read_size = obj->vData[j];
side_effect();
}
side_effect();
}
}
void vector_iterator_loop(const int n, VecX* obj)
{
for(int i=0; i!=n; ++i) {
side_effect();
for(std::vector<PodType>::const_iterator j=obj->vData.begin(), e=obj->vData.end(); j!=e; ++j) {
g_read_size = *j;
side_effect();
}
side_effect();
}
}
测试主文件
using namespace std;
volatile size_t g_read_size;
void side_effect()
{
g_read_size = 0;
}
typedef size_t Value;
template<typename Container>
Value average(Container const& c)
{
const Value sz = c.size();
Value sum = 0;
for(Container::const_iterator i=c.begin(), e=c.end(); i!=e; ++i)
sum += *i;
return sum/sz;
}
void take_timings()
{
const int x = 10;
const int n = 10*1000*1000;
VecX vobj;
vobj.vData.resize(container_size);
RawX robj;
robj.Resize(container_size);
std::vector<DWORD> raw_times;
std::vector<DWORD> vec_times;
std::vector<DWORD> rit_times;
std::vector<DWORD> vit_times;
for(int i=0; i!=x; ++i) {
const DWORD t1 = timeGetTime();
raw_loop(n, &robj);
const DWORD t2 = timeGetTime();
vector_loop(n, &vobj);
const DWORD t3 = timeGetTime();
raw_iterator_loop(n, &robj);
const DWORD t4 = timeGetTime();
vector_iterator_loop(n, &vobj);
const DWORD t5 = timeGetTime();
raw_times.push_back(t2-t1);
vec_times.push_back(t3-t2);
rit_times.push_back(t4-t3);
vit_times.push_back(t5-t4);
}
cout << "Average over " << x << " iterations for loops with count " << n << " ...\n";
cout << "The PodType is '" << typeid(PodType).name() << "'\n";
cout << "raw_loop: " << setw(10) << average(raw_times) << " ms \n";
cout << "vec_loop: " << setw(10) << average(vec_times) << " ms \n";
cout << "rit_loop: " << setw(10) << average(rit_times) << " ms \n";
cout << "vit_loop: " << setw(10) << average(vit_times) << " ms \n";
}
int main()
{
take_timings();
return 0;
}
由Visual Studio调试器显示生成的程序集(对于带有“迭代器”的2个函数。
* raw_iterator_loop *
void raw_iterator_loop(const int n, RawX* obj)
{
for(int i=0; i!=n; ++i) {
00 mov eax,dword ptr [esp+4]
00 test eax,eax
00 je raw_iterator_loop+53h (4028C3h)
00 push ebx
00 mov ebx,dword ptr [esp+0Ch]
00 push ebp
00 push esi
00 push edi
00 mov ebp,eax
side_effect();
00 call side_effect (401020h)
for(PodType *j=obj->pData, *e=obj->pData+size_t(obj->wCount); j!=e; ++j) {
00 movzx eax,word ptr [ebx+4]
00 mov esi,dword ptr [ebx]
00 lea edi,[esi+eax*2]
00 cmp esi,edi
00 je raw_iterator_loop+45h (4028B5h)
00 jmp raw_iterator_loop+30h (4028A0h)
00 lea esp,[esp]
00 lea ecx,[ecx]
g_read_size = *j;
00 movzx ecx,word ptr [esi]
00 mov dword ptr [g_read_size (4060B0h)],ecx
side_effect();
00 call side_effect (401020h)
00 add esi,2
00 cmp esi,edi
00 jne raw_iterator_loop+30h (4028A0h)
}
side_effect();
00 call side_effect (401020h)
00 sub ebp,1
00 jne raw_iterator_loop+12h (402882h)
00 pop edi
00 pop esi
00 pop ebp
00 pop ebx
}
}
00 ret
* vector_iterator_loop *
void vector_iterator_loop(const int n, VecX* obj)
{
for(int i=0; i!=n; ++i) {
00 mov eax,dword ptr [esp+4]
00 test eax,eax
00 je vector_iterator_loop+43h (402813h)
00 push ebx
00 mov ebx,dword ptr [esp+0Ch]
00 push ebp
00 push esi
00 push edi
00 mov ebp,eax
side_effect();
00 call side_effect (401020h)
for(std::vector<PodType>::const_iterator j=obj->vData.begin(), e=obj->vData.end(); j!=e; ++j) {
00 mov esi,dword ptr [ebx+4]
00 mov edi,dword ptr [ebx+8]
00 cmp esi,edi
00 je vector_iterator_loop+35h (402805h)
g_read_size = *j;
00 movzx eax,word ptr [esi]
00 mov dword ptr [g_read_size (4060B0h)],eax
side_effect();
00 call side_effect (401020h)
00 add esi,2
00 cmp esi,edi
00 jne vector_iterator_loop+21h (4027F1h)
}
side_effect();
00 call side_effect (401020h)
00 sub ebp,1
00 jne vector_iterator_loop+12h (4027E2h)
00 pop edi
00 pop esi
00 pop ebp
00 pop ebx
}
}
00 ret
答案 0 :(得分:11)
虽然我生成的机器代码版本与您的版本不同(MSVC ++ 2005),但两种变体之间的差异与代码中的差异非常相似:
在代码的矢量版本中,“end iterator”值被预先计算并存储为std::vector
对象的成员,因此内部循环只是加载随时可用的值。
在原始指针版本中,“end iterator”值在内循环的标头中显式计算(通过用于实现乘法的lea
指令),这意味着外循环的每次迭代都执行那个计算一次又一次。
如果您按如下方式重新实现raw_iterator_loop
(即将结束指针的计算拉出外部循环)
void raw_iterator_loop(const int n, RawX* obj)
{
PodType *e = obj->pData+size_t(obj->wCount);
for(int i=0; i!=n; ++i) {
side_effect();
for(PodType *j=obj->pData; j!=e; ++j) {
g_read_size = *j;
side_effect();
}
side_effect();
}
}
(甚至在你的班级中存储和维护结束指针)你最终应该进行更“公平”的比较。
答案 1 :(得分:2)
产生指令差异的一个具体原因是Visual C ++ vector
有成员_Myfirst
和_Mylast
(对应begin()
和end()
)简化循环设置。
在原始情况下,编译器必须进行实际的指针数学运算才能设置所需的开始和结束本地。
这很可能会使寄存器的使用变得更加复杂,从而使vector
代码更快。
答案 2 :(得分:1)
编译器无法知道side_effect()
不会更改obj->pData
。
存储局部变量中无法更改的内容可以加快紧密循环,尤其是当循环内部调用编译器无法分析的函数时。
p.S。:这会影响raw_loop
和vector_loop
。 raw_loop
可以使用局部变量“修复”,vector_loop
不能。它不能,因为向量内部会有一个数组指针 ,并且编译器也无法知道什么都不会改变向量内的数组指针。毕竟,side_effect
可以调用,例如push_back()
。当然,如果编译器可以内联任何循环函数,它可能会更好地优化。例如。如果使用的向量是一个局部变量,其地址在函数外部是未知的,并且只传递给循环函数。然后编译器可能再次知道side_effect
无法更改向量(因为它无法知道它的地址),并且更好地进行优化。 - &GT;如果要针对非内联案例优化编译器,请确保编译器无法内联函数。
答案 3 :(得分:1)
您的计时可能反映了初始raw_loop支付加载缓存的成本这一事实。如果你重新排序测试以首先进行矢量测试(或者你可以抛弃第一次迭代,或者让每个测试成为一个单独的程序),你会得到类似的时间吗。
答案 4 :(得分:0)
查看为内部循环生成的程序集,除了一次寄存器更改外,它们基本相同。我不希望在此基础上有任何时间差异。
g_read_size = *j;
00 movzx ecx,word ptr [esi]
00 mov dword ptr [g_read_size (4060B0h)],ecx
side_effect();
00 call side_effect (401020h)
00 add esi,2
00 cmp esi,edi
00 jne raw_iterator_loop+30h (4028A0h)
g_read_size = *j;
00 movzx eax,word ptr [esi]
00 mov dword ptr [g_read_size (4060B0h)],eax
side_effect();
00 call side_effect (401020h)
00 add esi,2
00 cmp esi,edi
00 jne vector_iterator_loop+21h (4027F1h)
我对代码时序有类似的问题,我无法解释两段代码的差异。我从来没有得到关于那个的明确答案,尽管最后我确信自己这是一个代码对齐的例子。 How can adding code to a loop make it faster?
答案 5 :(得分:0)
尝试将PodType从int
更改为size_t
。那里的转换使循环初始化代码复杂化。
答案 6 :(得分:0)
我原本期望优化器足够聪明以消除任何差异,但通常减少和比较为零比与非零指针的比较更快。
e.g。
void countdown_loop(int n, RawX* obj)
{
if (!n) return;
size_t const wc = obj->wCount;
if (!wc) return;
PodType* const first = obj->pData;
do {
side_effect();
size_t i = wc;
PodType* p = first;
do {
g_read_size = *p;
side_effect();
++p;
} while (--i);
side_effect();
} while (--n);
}
答案 7 :(得分:0)
我知道这已经很晚了,但有一个快速观察:未签名的指令可能会稍快一点。据我了解,硬件实现只是简单一点,因为没有符号位可以在增量或减量时改变。这只是P6微架构CPU的一个时钟周期,但这些都加起来了。
我希望size_t是无符号的,因为没有负面的'尺寸'。
您必须与芯片制造商分析器(英特尔VTUNE - 30天试用版; AMD CodeAnalyzer免费版)进行分析,因为有太多东西可以使用:管道停顿,缓存未命中,数据未对齐,存储负载依赖...
这项业务比44年前我在高中写的第一份Fortran课程要复杂得多。没有人在汇编程序中编程。一个真正令人头疼的是看着C程序生成的PA-Risc(90年代的HP Unix系统芯片)指令......操作不符合我的预期,因为代码生成器理解PA-Risc的内部操作中央处理器。这些说明很有趣,因为它们在CPU中有意义,但在我的纸上却没有。