这是一篇在网上看到的技术文章,它把一个原来需要6000毫秒的代码段优化到400毫秒,这种优化效果让我震撼,所以我决定把它翻译共享出来。
下面是原文链接:http://www.codeproject.com/Articles/381630/Code-optimization-tutorial-Part-1
简介:
这篇文章是尝试把代码优化技术介绍给软件开发者。为些,我们将探究各种优化的方法。
第一步,我选取一段容易理解的算法代码段,并在上面运用各种不同的优化方法。
我们要解决的问题是3n+1猜想(关于这个问题的详细解释大家可以点这个链接
3n+1问题详细),
我们执行一个从1到1000000的循环,每个数都套用这个函数:
直到n变成1,我们再执行下一个数,我们不需要从键盘上读取任何数据程序就可以打印结果,并且我们可以计算出运行的时间,用来做测试的设备是:
AMD Athlon 2 P340 Dual Core 2.20 GHz, 4 GB of RAM, Windows 7 Ultimate x64。测试的语言是:C#和C++
代码的第一个版本:为1到1000000中的每个数执行上面的算法,算法会产生一系列的数字,直到等于1时才会停止,归1的步骤数将会被记录下来,并且最大的步骤数将会被确定下来。
C++代码段:
for (int i = nFirstNumber; i < nSecondNumber; ++i)
{
int nCurrentCycleCount = 1;
long long nNumberToTest = i;
while (nNumberToTest != 1)
{
if ((nNumberToTest % 2) == 1)
{
nNumberToTest = nNumberToTest * 3 + 1;
}
else
{
nNumberToTest = nNumberToTest / 2;
}
nCurrentCycleCount++;
}
if (nCurrentCycleCount > nMaxCycleCount)
{
nMaxCycleCount = nCurrentCycleCount;
}
}
C#代码段:
for (int i = FirstNumber; i < SecondNumber; ++i)
{
int iCurrentCycleCount = 1;
long iNumberToTest = i;
while (iNumberToTest != 1)
{
if ((iNumberToTest % 2) == 1)
{
iNumberToTest = iNumberToTest * 3 + 1;
}
else
{
iNumberToTest = iNumberToTest / 2;
}
iCurrentCycleCount++;
}
if (iCurrentCycleCount > MaxCycleCount)
{
MaxCycleCount = iCurrentCycleCount;
}
}
我用Debug和Release模式以及32位和64位分别执行了这段代码,然后我每次都运行100次取平均值(单位是ms)。下面是运行的结果:
C++ Debug | C++ Release | C#Debug | C# Release | |
x86 version | 6882 | 6374 | 6358 | 5109 |
x64 versiong | 1020 | 812 | 1890 | 742 |
我们从表里面看到的第一点是,32位的程序要比64位的慢5到7倍,因为x64设计是一个寄存器可以存储一个long long数据,而x86而需要两个寄存器,所以在后面我们不再研究32位的程序。第二点是,Debug和Release所用的时间差距很大,特别是C#。第三点是,通过观察,C#编译器貌似比C++编译器在优化方面更胜一筹。
我提供的第一个优化动作是与数学操作符有关的,我用非传统的方式替换传统的方式。我们可以看到上面的代码里面只有三个复杂的数学操作符:%,*,/。首先我要优化的是%2操作,我们发现只要与0x1做&操作就可以判断一个数是否是奇数。所以当我们执行下面的替换时:
我提供的第一个优化动作是与数学操作符有关的,我用非传统的方式替换传统的方式。我们可以看到上面的代码里面只有三个复杂的数学操作符:%,*,/。首先我要优化的是%2操作,我们发现只要与0x1做&操作就可以判断一个数是否是奇数。所以当我们执行下面的替换时:
if ((nNumberToTest % 2) == 1)
替换成:
if ((nNumberToTest & 0x1) == 1)
下面是新的结果表:
C++ Debug | C++ Release | C# Debug | C# Relase |
922 | 560 | 1641 | 714 |
我们发现,C++ Release版本得到了很大的优化,Release和Debug版本的优化程序不同让我相信,在Release版本下编译器会删除多余的指令。C#则好像没有太大的优化。下一个优化的动作是/2操作,我们发现用位操作>>1与/2操作得到的是相同的效果,
所以当我们执行下面的替换时:
nNumberToTest = nNumberToTest / 2;
替换成:
nNumberToTest = nNumberToTest >> 1;
下面是新的结果:
C++ Debug | C++ Release | C# Debug | C# Relase |
821 | 555 | 1432 | 652 |
我们发现C++ Debug, C# Debug, C# Release有差不多65到200毫秒的优化,而C++ Release版本则好像没有优化一样,那是因为在这种模式下编译器已经做了这个转换操作。最我们能做的变换只能是把*换成+操作,
所以当我们执行下面的替换时:
nNumberToTest = nNumberToTest * 3 + 1;
替换成:
nNumberToTest = nNumberToTest + nNumberToTest + nNumberToTest + 1;
下面是新的结果:
C++ Debug | C++ Release | C# Debug | C# Relase |
820 | 548 | 1535 | 629 |
优化最大的是C# Rlease版本,而C++ Release版本次之。而C# Debug的性能不升反降,这是因为当前版本相比上个版本执行了更多的指令,而且这时编译没有优化这些指令。这是最后一个可以优化的数学操作符,而代码中还有一些传统代码,为了确定编译器是否会自动产生一些传统的移动指令,我决定用三元运算符?:来替换if语句。为了能执行上面的优化操作我们要修改代码段:如果数字是奇数,它的就要除以2,如果是偶数,它就要加上2n+1。根据修改我们可以得到下面的初始化函数形式:
用上面的等式,我们可以把两步操作合并成一步。我们要重写算法以便我们可以计算下一步的数值,假设现在的数是个偶数,然后我们存储这个数的最后一个字节,如果这个值为真,我们就增加当前循环的次数,并且把number+1加到下一个数值上(这个优化操作很重要,因为它与我下一篇要讲的sse优化有很大的关连)。
if ((nNumberToTest % 2) == 1)
{
nNumberToTest = nNumberToTest * 3 + 1;
}
else
{
nNumberToTest = nNumberToTest / 2;
}
nCurrentCycleCount++;
替换成:
int nOddBit = nNumberToTest & 0x1;
long long nTempNumber = nNumberToTest >> 1;
nTempNumber += nOddBit?nNumberToTest + 1:0;
nCurrentCycleCount += nOddBit?2:1;
nNumberToTest = nTempNumber;
下面是新的结果(用三元操作符替换if语句):
C++ Debug | C++ Release | C# Debug | C# Relase |
1195 | 462 | 1565 | 752 |
总结:
1.取模和除法操作会耗用较多的时间,应该想办法用别的操作来替换它们
2.仔细分析问题,并找出一个可以替代的解决方案来解决这个问题
3.尽量用别的方法来替换if语句,如果if语句只是在基于一种情况下得到几个不同的值
下篇文章主要在C#和C++环境下讲解“怎么让你的程序更快”
2.仔细分析问题,并找出一个可以替代的解决方案来解决这个问题
3.尽量用别的方法来替换if语句,如果if语句只是在基于一种情况下得到几个不同的值
下篇文章主要在C#和C++环境下讲解“怎么让你的程序更快”