C++中模板元编程原理及速度测试

这两天一直被这个模板元编程给迷住了,觉得它真是一个很好的东西!于是好奇就仔细的研究了下,之前看过几篇文章大概的意思就是“编可以编程的程序”。听起来很神奇吧。

其基本原理也就是让编译器在编译期间就计算好一些我们需要计算的值。在程序运行期间就不需要再去计算这些值了,从而提高程序的运行性能。当然这样做会让程序编译起来很慢,一般不常用。不过在一些需要的地方我们还是舍得编译的效率问题的。。

先写个例子看看其中的原理:

在上边的代码里,我们看模板结构里面传了俩整形的变量。结构体里面用来生成枚举值,我们可以先从理论上得到结论是在编译期间这些枚举值就会被计算出来。是一个透明的数据。而且最终得到的a应该等于103。我们先运行:

输出结果:103

结果没错,但是我们看不到到底是不是在编译期间完成的啊。。怎么办呢? 于是在 int a = ADD< 2, 3>::Result; 执行之前设个断点,或者按F10直接断在main函数开头。我们来查看反汇编代码:

00411B00 push ebp
00411B01 mov ebp,esp
00411B03 sub esp,0CCh
00411B09 push ebx
00411B0A push esi
00411B0B push edi
00411B0C lea edi,[ebp-0CCh]
00411B12 mov ecx,33h
00411B17 mov eax,0CCCCCCCCh
00411B1C rep stos dword ptr [edi]

00411B1E mov dword ptr [a],67h


00411B25 push offset string "pause" (4260C8h)
00411B2A call @ILT+565(_system) (41123Ah)
00411B2F add esp,4

00411B32 xor eax,eax

上边是整个main函数的反汇编代码,我们可以看到上边红色的那句汇编代码就是给a 赋枚举值Result,看直接MOV的是67h = 103!由此可以证明我们的枚举值是在编译期间就计算好了的。。

前面看过一篇文章,是关于用模板元来解循环。我们以求一个整形数组所有元素之和为例子写了一个测试:

#include <iostream>

using namespace std;

template< int count, class Ty >
class ADD
{
public:
static int Result( Ty* elem )
{
return elem[ 0 ] + ADD< count - 1, Ty >::Result( elem++ );
}
};

template< class Ty >
class ADD< 1, Ty >
{
public:
static int Result( Ty* elem )
{
return elem[ 0 ];
}
};

int main( void )
{
__int64 BegTime = 0;
__int64 EndTime = 0;
__int32 sum = 0;

int a[ 3 ] = { 4, 5, 6 };

__asm
{
rdtsc
mov dword ptr [ BegTime ], eax
lea eax, dword ptr [ BegTime ]
mov dword ptr [ eax + 4 ], edx
}


ADD< 3, int >::Result( a );

__asm
{
rdtsc
mov dword ptr [ EndTime ], eax
lea eax, dword ptr [ EndTime ]
mov dword ptr [ eax + 4 ], edx
}

cout << EndTime - BegTime << endl;

BegTime = 0;
EndTime = 0;

__asm
{
rdtsc
mov dword ptr [ BegTime ], eax
lea eax, dword ptr [ BegTime ]
mov dword ptr [ eax + 4 ], edx
}

for ( int i = 0; i < 3; ++i )
{
sum += a[ i ];
}

__asm
{
rdtsc
mov dword ptr [ EndTime ], eax
lea eax, dword ptr [ EndTime ]
mov dword ptr [ eax + 4 ], edx
}

cout << EndTime - BegTime << endl;

system( "pause" );
return 0;
}

上边的程序中红色部分表示模板元终结模板吧,我是这样理解的。。名字无所谓啦 - - 它的意思是当我程序执行到模板参数一为1的情况写就终结调用了。 当然可以参数2或者多个限定。过程是这样的:

调用顺序为:ADD<3, int>::Result( int* ) => ADD<2, int>::Result( int*) => ADD<1, int>::Result( int* )相当于转换成递归了 。。。

那我们在编译期间是不是编译器就已经递归调用了函数计算出结果,我们在运行时只需要取这个值这么简单呢?我们断在ADD< 3, int >::Result( a ); 之前或它上边,来看看反汇编代码:

0041B291 lea eax,[a]
0041B294 push eax
0041B295 call ADD<3,int>::Result (419A82h)
0041B29A add esp,4

我们在蓝色的地方F11跟进去,

一直跟到这里:

0041BFCE mov eax,dword ptr [elem]
0041BFD1 mov dword ptr [ebp-0C4h],eax
0041BFD7 mov ecx,dword ptr [elem]
0041BFDA add ecx,4
0041BFDD mov dword ptr [elem],ecx
0041BFE0 mov edx,dword ptr [ebp-0C4h]
0041BFE6 push edx
0041BFE7 call ADD<2,int>::Result (4193CFh)
0041BFEC add esp,4
0041BFEF mov ecx,dword ptr [elem]
0041BFF2 add eax,dword ptr [ecx]

我们再在蓝色的地方跟进去,

一直到这里:

0041C6AE mov eax,dword ptr [elem]
0041C6B1 mov dword ptr [ebp-0C4h],eax
0041C6B7 mov ecx,dword ptr [elem]
0041C6BA add ecx,4
0041C6BD mov dword ptr [elem],ecx
0041C6C0 mov edx,dword ptr [ebp-0C4h]
0041C6C6 push edx
0041C6C7 call ADD<1,int>::Result (41A063h)
0041C6CC add esp,4
0041C6CF mov ecx,dword ptr [elem]
0041C6D2 add eax,dword ptr [ecx]

我们再在蓝色的地方跟进去,

一直到这里:

0041CAE0 push ebp
0041CAE1 mov ebp,esp
0041CAE3 sub esp,0C0h
0041CAE9 push ebx
0041CAEA push esi
0041CAEB push edi
0041CAEC lea edi,[ebp-0C0h]
0041CAF2 mov ecx,30h
0041CAF7 mov eax,0CCCCCCCCh
0041CAFC rep stos dword ptr [edi]

0041CAFE mov eax,dword ptr [elem]
0041CB01 mov eax,dword ptr [eax]

看到整个过程了吧,这里可以明白一点就是在上边的汇编代码里面我标注成红色的数字是在编译期间计算好了的。我们在定位的时候直接就传入了模板元在编译期间计算后的数字。但是在这里并没有看到整个调用过程在编译期间执行。之前看过的文章说会在编译期间执行也不知道为什么。我们上边的测试是可以在运行期间断下来的并且执行了递归调用。

__asm
{
rdtsc
mov dword ptr [ BegTime ], eax
lea eax, dword ptr [ BegTime ]
mov dword ptr [ eax + 4 ], edx

}

这段代码在前面的博文中已经提到过,是获取程序运行以来的时间(纳秒级)。

这里测试我们模板元解开循环后的速度和循环的速度。

程序输出结果为:

3090周期

130周期

这里的周期除以CPU主频率得到运行的时间,这里不用计算就能明显区分出来了 - -

可见解开循环只是变成了递归,真正起到作用的我认为是在编译期间计算好了每次递归所应该传入的整形数值。但是递归开销很大,而且很容易堆栈溢出。。所以这个问题还得我们深思。。不过在编译期间运算一些比如第一个例子计算枚举或类似的操作时,模板元的好处就得以体现了。。

有什么说的不对的地方望大家指教。。我是菜鸟 - -

你可能感兴趣的:(C++)