优化C++软件

Posted wuhui_gdnt

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了优化C++软件相关的知识,希望对你有一定的参考价值。

7.6. 指针与引用

指针与引用

指针与引用同样高效,因为它们实际上做相同的事情。例如:

// Example 7.12

void FuncA (int * p)

    *p = *p + 2;

void FuncB (int & r)

     r = r + 2;

这两个函数做同样的事情,如果你看编译器产生的代码,你将注意到对这两个函数,代码实际上是相同的。区别只是事关编程风格。使用指针而不是引用的好处有:

  • 在你看上面的函数体时,很清楚p是一个指针,但r是一个引用还是普通变量就不清楚了。使用指针,使读者搞清楚发生了什么。
  • 使用指针可以做使用引用无法完成的事情。你可以改变一个指针的指向,并且你可以使用指针进行算术操作。

使用引用而不是指针的好处有:

  • 在使用引用时语法更简单。
  • 引用的使用比指针更安全,因为在大多数情形里,它们确定指向一个有效地址。如果没有初始化,如果指针算术计算超出有效地址访问,或者如果指针被类型转换到错误的类型,指针是无效的,且导致致命错误。
  • 对拷贝构造函数与重载操作符,引用是有用的。
  • 声明为常量引用的函数参数接受表达式作为实参,而指针与非常量引用要求一个变量。

效率

通过指针或引用访问一个变量或对象可能与直接访问它一样快。这个效率的原因在于微处理器构造的方式。所有声明在函数里的非静态变量与对象保存在栈上,实际上通过栈指针相对寻址。类似的,通过在C++称为this的隐含指针,访问声明在一个类里的非静态变量与对象。因此,我们可以得出结论,在一个结构良好的C++程序里,大多数变量实际上是通过指针以这样或那样的方式访问。因此,微处理器必须设计成使指针高效,而它们正是那样。

不过,使用指针与引用也有坏处。最重要的,它要求额外的寄存器来保存指针或引用的值。寄存器是稀缺资源,特别在32位模式里。如果没有足够的寄存器,每次使用时,指针必须从内存载入,这将使程序变慢。另一个坏处是,在指向的变量可以访问前的几个时钟周期,就需要指针的值。

指针算术

指针实际上是保存一个内存地址的整数。因此,指针算术操作与整数算术操作一样快。在一个整数加上一个指针时,其值乘以指向对象的大小。例如:

// Example 7.13

struct abc int a; int b; int c;;

abc * p; int i;

p = p + i;

这里,加到p的值不是i,而是i*12,因为abc的大小是12字节。因此,把i加到p所需的时间等于进行一次乘法与一次加法的时间。如果abc的大小是2的指数,可以一个快得多的偏移操作替代乘法操作。在上面的例子中,可以通过向结构体增加一个整数,把abc的大小增加到16字节。

递增或递减一个指针不要求乘法,仅要求一次加法。比较两个指针仅要求一次快速的整数比较。计算两个指针间的差要求一次慢速的除法,除非指向对象类型的大小是2的指数(关于除法,参考第140页)。

在指针值被计算出来后大约2时钟周期,可以访问指向的对象。因此,建议在使用指针,提早计算指针值。例如,x = *(p++)x = *(++p)更高效,因为后者中对x的读,在指针p被递增后,必须等待几个时钟周期,而前者可以在递增p之前读x。递增与递减操作符的更多讨论,参考第25页。

7.7. ​​​​​​​函数指针

如果目标地址可以预测,通过函数指针调用函数,通常比直接调用该函数多几个时钟周期。如果函数指针的值与该语句上一次执行时相同,目标地址被预测。如果函数指针的已经改变,那么目标地址很可能被误预测,这会导致长时延。分支预测参考第44页。如果函数指针的改变遵循一个简单、规律的模式,Pentium M处理器可能能够预测,而Pentium 4AMD处理器,每次函数指针改变,肯定做出一次误预测。

​​​​​​​7.8. 成员指针

在简单的情形里,数据成员指针只保存数据成员相对于该对象开头的偏移,成员函数指针只是成员函数的地址。但存在特殊的情形,比如需要复杂得多的实现的多继承。绝对应该避免这些复杂的情形。

编译器必须使用成员指针最复杂的实现,如果它没有关于这个成员指针所援引类的完整信息。例如:

// Example 7.14

class c1;

int c1::*MemberPointer;

这里,在MemberPointer声明的时候,编译器除了名字,没有关于类c1的信息,因此,它必须假定最坏的可能的情形,并制作该成员指针的一个复杂实现。在声明MemberPointer之前,完成c1的完整声明,可以避免这。避免多继承、虚拟函数及其他降低成员指针效率的复杂性。

大多数C++编译器有各种选项来控制实现成员指针的方式。尽可能使用给出最简单可能实现的选项,确保对所有使用同一个成员指针的模块使用相同的编译器选项。

7.9. ​​​​​​​智能指针

智能指针是行为类似于指针的对象。它有指针被删除时,指向的对象也被删除的特殊性质。智能指针仅用于保存在使用new动态分配内存里的对象。使用智能指针的目的是确保,在对象不再使用时,对象被正确删除,内存被释放。智能指针可以被视为一个仅包含单个元素的容器。

智能指针最常见的实现是auto_ptrshared_ptrAuto_ptr总是有仅仅有一个auto_ptr拥有被分配的对象,通过赋值所有权从一个auto_ptr转移到另一个auto_ptrShared_ptr允许多个指针指向同一个对象。

通过智能指针访问一个对象没有额外的代价。不管p是一个简单指针还是智能指针,通过*pp->member访问对象同样快。但在创建、删除、拷贝或从一个函数传递到另一个时有额外的代价。对shared_ptr,这些代价比auto_ptr高。

在程序的逻辑结构决定对象必须由一个函数动态创建,随后由另一个函数删除,且这两个函数彼此无关(不是同一个类的成员),智能指针是有用的。如果同一个函数或类负责创建及删除对象,那么你不需要智能指针。

如果程序使用许多小的动态分配对象,每个有一个智能指针,那么你可以考虑这个解决方案代价是否太高。将所有的对象保存在一个容器里,最好使用连续内存,会更高效。参考第94页关于容器类的讨论。

7.11. ​​​​​​​数组

数组只是被实现为将元素连续第保存在内存里。数组维度信息不保存。这使得在CC++中数组的使用比其他编程语言更快,但不那么安全。通过定义一个行为类似带有边界检查数组的容器类,可以克服这个安全问题,如这个例子所示:

// Example 7.15a. Array with bounds checking

template <typename T, unsigned int N> class SafeArray

protected:

      T a[N];                                                                 // Array with N elements of type T

public:

      SafeArray()                                                        // Constructor

            memset(a, 0, sizeof(a));                             // Initialize to zero

     

      int Size()                                                           // Return the size of the array

            return N;

     

      T & operator[] (unsigned int i)                    // Safe [] array index operator

            if (i >= N)

                  // Index out of range. The next line provokes an error.

                  // You may insert any other error reporting here:

                  return *(T*)0;       // Return a null reference to provoke error

           

            // No error

            return a[i];                   // Return reference to a[i]

     

;

更多容器类的例子在www.agner.org/optimize/cppexamples.zip中给出。

通过说明作为模板参数的类型与大小,声明使用上面模板类的数组,如下面的例子7.15b。就像一个普通的数组,使用中括号索引访问它。构造函数将所有元素置零。你可以删除memset行,如果你不希望这个初始化,或者类型T是一个带有执行必须初始化的缺省构造函数的类。编译器可能报告memset是弃用的。这是因为如果参数大小不对,它会导致错误,但它仍然是将数组置零最快的方式。如果索引超出范围,[]操作符将检测到错误(关于边界检查,参考第137页)。这里如果返回一个空引用,以一个相当不方便的方式诱发一条错误消息。在一个保护操作系统中,如果访问这个数组元素,这将诱发一条错误消息,这个错误很容易通过调试器追踪。你可以通过其他形式的错误报告替换这行。例如,在Windows中,你可以写FatalAppExitA(0,"Array index out of range");,或者更好的,制作你自己的错误消息函数。

下面的例子展示了如何使用SafeArray

// Example 7.15b

SafeArray <float, 100> list;                  // Make array of 100 floats

for (int i = 0; i < list.Size(); i++)         // Loop through array

      cout << list[i] << endl;                   // Output array element

由一个列表初始化的数组最好是静态的,如第20页解释。可以通过使用memset初始化数组:

// Example 7.16

float list[100];

memset(list, 0, sizeof(list));

多维数组应该组织为最后的索引变化最快:

// Example 7.17

const int rows = 20, columns = 50;

float matrix[rows][columns];

int i, j; float x;

for (i = 0; i < rows; i++)

      for (j = 0; j < columns; j++)

             matrix[i][j] += x;

这确保元素被顺序访问。这两个循环相反的次序使访问不连续,这使得数据缓存效率下降。

如果非顺序索引行,除了第一个维度,其他维度的大小最好是2的指数, 使地址计算更高效:

// Example 7.18

int FuncRow(int); int FuncCol(int);

const int rows = 20, columns = 32;

float matrix[rows][columns];

int i; float x;

for (i = 0; i < 100; i++)

      matrix[FuncRow(i)][FuncCol(i)] += x;

这里,代码必须计算(FuncRow(i)*columns + FuncCol(i)) * sizeof(float),以算出矩阵元素的地址。在这个情形里,在columns2的指数时,乘columns会更快。在前面的例子中,这不是一个问题,因为优化编译器会看到行被顺序访问,通过前面行的地址加上行的长度来计算每行的地址。

同样的建议适用于结构体或类对象数组。如果非顺序访问元素,对象的大小最好是2的指数。

使列数成为2的指数的建议不总是适用于比1级数据缓存大且非顺序访问的数组,因为它会导致缓存竞争。这个问题的讨论参考第88页。

7.11. ​​​​​​​类型转换

C++语法有几种进行类型转换的方式:

// Example 7.19

int i; float f;

f = i; // Implicit type conversion

f = (float)i; // C-style type casting

f = float(i); // Constructor-style type casting

f = static_cast<float>(i); // C++ casting operator

这些不同的方法有实际相同的效果。使用哪个方法是编程风格问题。下面讨论不同类型转换的时间消耗。

有符号/无符号转换

// Example 7.20

int i;

if ((unsigned int)i < 10) ...

有符号与无符号整数间的转换只是让编译器以不同的方式解释整数的比特。不检查溢出,代码不需要额外时间。这些转换可以自由使用,没有性能代价。

整数大小转换

// Example 7.21

int i; short int s;

i = s;

一个整数如果是有符号的,通过扩展符号位,如果是无符号的,通过零扩展,转换到更长的类型。这通常需要1时钟周期。如果源是一个算术表达式。大小转换通常不需要额外时间,如果它与读取内存中一个变量的值相关,如例子7.22

// Example 7.22

short int a[100]; int i, sum = 0;

for (i=0; i<100; i++) sum += a[i];

通过忽略更高位,将整数转换到更小的类型、不检查溢出。例如:

// Example 7.23

int i; short int s;

s = (short int)i;

这个转换不需要额外时间。它只是保存32位整数的低16位。

浮点精度转换

在使用浮点寄存器栈时,floatdoublelong double间的转换不需要额外时间。在使用XMM寄存器时,需要215时钟周期(依赖于处理器)。寄存器栈与XMM寄存器的对比,参考第25页。例子:

// Example 7.24

float a; double b;

a += b;

在这个例子中,如果使用XMM寄存器,转换是高代价的。Ab应该是相同类型以避免之。进一步讨论参考第143页。

整数到浮点转换

有符号整数到floatdouble的转换需要4 ~ 6时钟周期,依赖于处理器与使用的寄存器类型。无符号整数的转换需要更长时间。如果没有溢出的危险,首先把无符号整数转换到有符号整数会更快的:

// Example 7.25

unsigned int u; double d;

d = (double)(signed int)u; // Faster, but risk of overflow

有时可以使用浮点变量替换整数变量,避免整数到浮点转换。例子:

// Example 7.26a

float a[100]; int i;

for (i = 0; i < 100; i++) a[i] = 2 * i;

在这个例子里,通过一个额外的浮点变量,可以避免i到浮点的转换:

// Example 7.26b

float a[100]; int i; float i2;

for (i = 0, i2 = 0; i < 100; i++, i2 += 2.0f) a[i] = i2;

浮点到整数转换

浮点值到整数的转换需要非常长的时间,除非启用SSE2或更新的指令集。通常,转换需要50 ~ 100时钟周期。原因是C/C++标准指定截断,因此浮点取整模式必须改变为截断,再改回来。

如果在代码的关键部分存在浮点到整数转换,那么对它做些什么是重要的。可能的方案有:

  • 使用不同类型的变量,避免转换。
  • 将中间结果保存为浮点,将转换移出最里层循环。
  • 使用64位模式或启用SSE2指令集(要求支持这的微处理器)。
  • 使用取整替代截断,制作一个使用汇编语言的取整函数。关于取整的细节参考第144页。

指针类型转换

指针可以被转换到另一个类型的指针。类似的,指针可以转换到整数,或者整数可以转换到指针。整数有足够的比特保存指针是重要的。

这些转换不会产生额外的代价。它只是事关以不同的方式或绕过语法检查,解释相同比特。

当然,这些转换不安全。确保结果有效是程序员的责任。

重新解释对象的类型

通过类型转换其地址,使编译器将一个变量或对象处理为另一个类型,是可能的:

// Example 7.27

float x;

*(int*)&x |= 0x80000000; // Set sign bit of x

这里,语法看起来有些奇怪。X的地址被类型转换为一个整数指针,然后解引用这个指针,作为整数访问x。实际上制作一个指针,编译器不产生任何额外的代价。这个指针只是被优化掉,结果x被处理为一个整数。但&操作符强制编译器在内存而不是寄存器里保存x。上面例子通过使用只能应用于整数的|操作符设置x的符号位。它比x = -abs(x);更快。

在类型转换指针时,要小心若干危险:

  • 违反标准C说明不同类型的两个指针不能指向相同的对象(除了char指针)的严格别名规则的技巧。优化编译器可能在两个不同的寄存器中保存浮点与整数表示。你需要检查编译器的行为是否就是你所需。使用联合更安全,如第146页的例子14.23
  • 如果对象被处理作比实际更大,该技巧会失败。上面这个代码将失败,如果intfloat使用更多比特(在x86系统里,两者都使用32比特)。
  • 如果访问一个变量的部分,例如一个64位双精度的32位,对使用大端储存的平台,代码将不可移植。
  • 如果分部分访问一个变量,例如一次写一个64位双精度的32位,这个代码可能比预期慢,因为CPU里的一个写转发时延(参考手册3IntelAMDVIA CPU的微架构》)。

Const转换

Const_cast操作符用于从一个指针免除const限定。它有一些语法检查,因此,比C形式的类型转换更安全,无需添加任何额外的代码。例如:

// Example 7.28

class c1

      const int x;                                   // constant data

public:

      c1() : x(0) ;                                // constructor initializes x to 0

      void xplus2()                            // this function can modify x

            *const_cast<int*>(&x) += 2;  // add 2 to x

;

这里,const_cast操作符的效果是删除x上的const限定。它是免除语法限定的一个方式,但它不产生任何额外的代码且不需要额外的时间。这是确保一个函数可以修改x,其他函数不能的一个有用的方式。

Static转换

Static_cast操作符做的与C形式的类型转换相同。例如,它用于将float转换为int

Reinterpret转换

Reinterpret_cast操作符用于指针转换。它做的与带有更多一点语法检查的C形式类型转换相同。它不产生任何额外的代码。

Dynamic转换

Dynamic_cast操作符用于将一个类指针转换为另一个类的指针。它对转换的有效性进行运行时检查。例如,在一个基类指针被转换为派生类的指针时,它检查原始指针是否真的指向派生类的一个对象。这个检查使得dynamic_cast比简单的类型转换更耗时些,但也更安全。它可能捕捉到原本不被发现的编程错误。

转换类对象

涉及类对象的转换(而不是对象指针)是看可能的,如果程序员定义了说明如何进行这个转换的一个构造函数、一个重载赋值操作符或一个重载类型转换操作符。构造函数或重载操作符与成员函数效率相同。

7.12. ​​​​​​​分支与switch语句

通过使用流水线,在执行指令之前的几个阶段中获取并解码指令,现代微处理器获得高速。不过,流水线结构有一个大问题。一旦代码有一个分支(比如一个if-else结构),微处理器预先不知道向流水线输入哪个分支。如果错误的分支输入流水线,那么直到10 ~ 20时钟周期后,这个错误才被检测到,而在这期间完成的获取、解码以及可能推测执行指令工作纯是浪费。一旦微处理器将一个分支输入流水线,随后发现选择了错误的分支,结果是它浪费了几个时钟周期。

微处理器设计者为减少这个问题,付出了很大的努力。最重要的方法是分支预测。基于该分支以及其他附近分支的历史,现代微处理器使用先进的算法来预测分支的方向。对每种微处理器,这些用于分支预测的算法是不相同的。这些算法在手册3IntelAMDVIA CPU微架构》中详细描述。

在微处理器做出了正确的预测时,分支指令通常需要0 ~ 2时钟周期。从分支误预测恢复的时间大约是12 ~ 25时钟周期,依赖于处理器。这称为分支误预测惩罚。

如果在大多数时间里都被预测到,分支的代价相对不高,但如果经常误预测,代价就高昂了。当然,总是去往相同方向的分支能良好预测。一个大多数时间去往一个方向,很少去另一个分析的分支,仅在它去往另一个方向时被误预测。一个去往一个方向许多次,然后去往另一个方向许多次的分支,仅在改变方向时被误预测。遵循一个简单周期性模式的分支也可以被相当好地预测,如果它在一个具有很少或没有其他分支的循环里。例如,一个简单周期性模式可以去往一个方向两次,另一个方向三次。然后再两次第一个方向,三次另一个方向,以此类推。最坏的情形是,分支随机去往一个方向,某个方向都有50%的机会。这样的分支将在50%时间里被误预测。

一个for循环或while循环也是一种分支。在每次迭代后,决定是重复还是退出循环。循环分支通常预测良好,如果重复计数小且总是相同。依赖于处理器,在964之间变化的最大循环计数可以被完美预测。嵌套循环仅在某些处理器上被良好预测。在许多处理器上,一个包含几个分支的循环不能被良好预测。

Swtich语句是一种去往超过2个方向的分支。Switch语句是最高效的,如果case标签遵循一个序列,其中每个标签等于前面标签加一,因为它可以被实现为一个目标跳转表。带有许多彼此远离标签的switch语句是低效的,因为编译器必须将它转换为一棵分支树。

在较旧的处理器上,带有连续标签的switch语句只是被预测去往上次执行方向。因此,一旦它去往不同于上次的方向,就肯定被误预测。较新的处理器有时能够预测switch语句,如果它遵循一个简单周期性的模式,或者如果它与前面的分支相关且不同目标的数量少。

在程序的关键部分中,最好将分支与switch语句保持在很小数量,特别在分支预测不好时。如果可以消除分支,展开一个循环是有帮助的,如下一节所解释的那样。

分支与函数调用的目标被保存在一个特殊的称为分支目标缓冲的缓存里。如果程序有许多分支或函数调用,会出现分支目标缓冲冲突。这样冲突的结果是,分支会被误预测,即使它们本来可以被良好预测。甚至函数调用也会因为这个原因被误预测。因此,在代码关键部分带有许多分支与函数调用的程序会遭受误预测。

在某些情形里,使用查找表替换一个可预测性不好的分支是可能的。例如:

// Example 7.29a

float a; bool b;

a = b ? 1.5f : 2.6f;

这里?:操作符是一个分支。如果它的可预测性不好,使用一个查找表替换它。

// Example 7.29b

float a; bool b = 0;

const float lookup[2] = 2.6f, 1.5f;

a = lookup[b];

如果bool被用作一个数组索引,确保它被初始化或来自一个可靠的来源,使它不会有01以外的值是重要的。参考第27页。

在某些情形里,依赖于特定的指令集,编译器可以自动使用一个条件移动替换一个分支。

137页与138页上的例子显示了减少分支数的各种方法。手册3IntelAMDVIA CPU的微架构》给出了不同微处理器中分支预测的更多细节。

7.13. ​​​​​​​循环

循环的效率依赖于微处理器可以多好地预测循环控制分支。参考前面的章节以及手册3IntelAMDVIA CPU微架构》。具有小且固定重复计数以及不包含分支的循环可以被完美预测。如上面解释的,可以被预测的最大循环计数取决于处理器。仅某些具有特殊循环预测器的处理器可以预测嵌套循环。在其他处理器上,仅最里层循环可以良好预测。具有高重复计数的循环仅在退出时被误预测。例如,如果一个循环重复1000次,在1000次里循环控制分支仅误预测一次,因此对总体执行时间,误预测惩罚的贡献微不足道。

循环展开

在某些情形里,展开一个循环是有好处的。例如:

// Example 7.30a

int i;

for (i = 0; i < 20; i++)

     if (i % 2 == 0)

          FuncA(i);

    

     else

          FuncB(i);

    

     FuncC(i);

这个循环重复20次,交替调用FuncAFuncB,然后FuncC。以2展开该循环得到:

// Example 7.30b

int i;

for (i = 0; i < 20; i += 2)

     FuncA(i);

     FuncC(i);

     FuncB(i+1);

     FuncC(i+1);

这有3个好处:

  • I < 20循环控制分支执行10次而不是20次。
  • 重复计数从20降到10的事实意味着在Pentium 4上它可以被完美预测。
  • 消除了if分支。

循环展开也有缺点:

  • 展开的循环在代码缓存或微操作缓存中占据更多空间。
  • Core2处理器在非常小循环上表现极好(少于65字节代码)。
  • 如果重复计数是奇数且以2展开,那么有一次必须在循环外完成的额外迭代。一般来说,在重复计数不确定能被展开因子整除时,你有这个问题。

循环展开仅应该在可以获得特定好处时才使用。如果循环包含浮点计算且循环计数器是整数,那么通常你可以假设总体执行时间由浮点代码确定,而不是循环控制分支。在这个情形里,展开循环没有好处。

在带有微操作缓存的处理器上(比如Sandy Bridge),最好避免循环展开,因为经济地使用微操作缓存是重要的。

编译器将通常自动展开一个循环,如果这看起来有利可图(参考第71页)。程序员不需要手动展开循环,除非可以获得特定的好处,比如消除例子7.30b中的if分支。

循环控制条件

最高效循环控制条件是一个简单的整数计数器。具有乱序能力的微处理器(参考第105页)将能够提前几次迭代对循环控制语句求值。

如果循环控制分支依赖于循环内的计算,它的效率下降。下面的例子将一个零结尾的ASCII字符串转换到小写:

// Example 7.31a

char string[100], *p = string;

while (*p != 0) *(p++) |= 0x20;

如果该字符串的长度已知,那么使用一个循环计数器更高效:

// Example 7.31b

char string[100], *p = string; int i, StringLength;

for (i = StringLength; i > 0; i--) *(p++) |= 0x20;

循环控制分支依赖循环内计算的一个常见情形是在数学迭代里,比如泰勒展开与Newton-Raphson迭代。这里,迭代被重复,直到残留误差低于特定的容许值。计算残留误差以及将它与容许值比较所需的时间可能很高,确定最坏情形的最大重复次数并总是迭代这个次数,效率会更高。这个方法的好处是,微处理器可以提前执行循环控制分支,并早在循环内浮点计算完成前,解决任何分支误预测。如果典型的重复计数接近最大重复计数,且每次迭代残留误差的计算对总体计算时间贡献显著时,这个方法是有优势的。

循环计数器最好应该是一个整数。如果循环需要浮点计数器,那么制作一个额外整数计数器。例如:

// Example 7.32a

double x, n, factorial = 1.0;

for (x = 2.0; x <= n; x++) factorial *= x;

通过增加一个整数计数器并在循环控制条件中使用整数,可以改善之:

// Example 7.32b

double x, n, factorial = 1.0; int i;

for (i = (int)n - 2, x = 2.0; i >= 0; i--, x++) factorial *= x;

注意,在具有多个计数器的循环里逗号与分号间的差别,就像在例子7.23b中。For循环有3个分句:初始化,条件与递增。这三个分句由分号隔开,而每个分句里的多个语句由逗号隔开。在条件分句应该仅有一个语句。

将整数与零比较有时比与其他数比较更高效。因此,使循环计数递减为零比递增到某个正数n要稍微高效一些。但如果循环计数器用作一个数组的索引,就不成立。数据缓存是对前向访问数组优化的,而不是后向。

拷贝或清零数组

对平凡的任务,比如拷贝数组或将数组置零,使用循环可能不是最优的。例如:

// Example 7.33a

const int size = 1000; int i;

float a[size], b[size];

// set a to zero

for (i = 0; i < size; i++) a[i] = 0.0;

// copy a to b

for (i = 0; i < size; i++) b[i] = a[i];

使用函数memset与memcpy通常更快:

// Example 7.33b

const int size = 1000;

float a[size], b[size];

// set a to zero

memset(a, 0, sizeof(a));

// copy a to b

memcpy(b, a, sizeof(b));

大多数编译器将自动使用对memset与memcpy调用替换这样的循环,至少在简单的情形里。Memset与memcpy的显式使用是不安全的,因为如果参数比目标数组更大,会出现严重的错误。但如果循环计数太大,相同的错误也会发生。

以上是关于优化C++软件的主要内容,如果未能解决你的问题,请参考以下文章

优化C++软件——目录

优化C++软件——目录

Spark SQL CBO 基于代价的优化

优化C++软件

优化C++软件

优化C++软件