C 和 C++ 性能改进代码优化的 10 个技巧

举报
Tiamo_T 发表于 2021/09/27 16:10:22 2021/09/27
【摘要】 当您开始使用 C、C++ 或任何其他编程语言编写代码时,您的首要目标可能是编写一个有效的程序。完成此操作后,您应该考虑以下几点来增强您的程序。程序的安全性内存消耗程序的速度(性能改进)本文将提供一些关于如何提高程序速度的高级想法。要记住的几个一般要点:您可以使用所有可能的技术优化代码以提高性能,但这可能会生成一个更大的文件,占用更大的内存。您可能有两个不同的优化目标,它们有时可能会相互冲突。...

当您开始使用 C、C++ 或任何其他编程语言编写代码时,您的首要目标可能是编写一个有效的程序。

完成此操作后,您应该考虑以下几点来增强您的程序。

  1. 程序的安全性
  2. 内存消耗
  3. 程序的速度(性能改进)


本文将提供一些关于如何提高程序速度的高级想法。

要记住的几个一般要点:

  • 您可以使用所有可能的技术优化代码以提高性能,但这可能会生成一个更大的文件,占用更大的内存。
  • 您可能有两个不同的优化目标,它们有时可能会相互冲突。例如,优化代码以提高性能可能与优化代码以减少内存占用和大小发生冲突。你可能需要找到一个平衡点。
  • 性能优化是一个永无止境的过程。您的代码可能永远不会被完全优化。总是有更多的改进空间来使您的代码运行得更快。
  • 有时我们可以使用某些编程技巧来使代码运行得更快,但代价是不遵循编码标准等最佳实践。尽量避免使用廉价技巧来使代码运行得更快。

1. 使用合适的算法优化你的代码

对于您编写的任何代码,您应该始终花一些时间思考并选择正确的算法以用于您的特定场景。

这个例子我们要分析的问题是在二维段中找到函数的最大值。

我们将只考虑整数。


首先,我们将在不考虑性能的情况下编写程序。然后,我们将讨论几种提高该程序性能的方法。

我们的场景:我们有 x [-100…100] 的区间和 y [-100…100] 的区间。现在在这两个区间中,我们正在寻找函数 (x*x + y*y)/(y*y + b) 的最大值。

这是两个变量的函数:x 和 y。还有一个可能不同的常量,用户将输入它。这个常数 b 总是大于 0,也小于 1000。

在我们的程序中,我们不会使用 math.h 库中实现的函数 pow()。找出哪种方法可以创建更快的代码将是一个有趣的练习。

示例代码:

#include <iostream>

#define LEFT_MARGINE_FOR_X -100.0
#define RIGHT_MARGINE_FOR_X 100.0
#define LEFT_MARGINE_FOR_Y -100.0
#define RIGHT_MARGINE_FOR_Y 100.0

using namespace std;

int
main(void)
{
//Get the constant value
cout<<"Enter the constant value b>0"<<endl;
cout<<"b->"; double dB; cin>>dB;

if(dB<=0)   return EXIT_FAILURE;
if(dB>1000) return EXIT_FAILURE;

//This is the potential maximum value of the function
//and all other values could be bigger or smaller
double dMaximumValue = (LEFT_MARGINE_FOR_X*LEFT_MARGINE_FOR_X+LEFT_MARGINE_FOR_Y*LEFT_MARGINE_FOR_Y)/ (LEFT_MARGINE_FOR_Y*LEFT_MARGINE_FOR_Y+dB);

double dMaximumX = LEFT_MARGINE_FOR_X;
double dMaximumY = LEFT_MARGINE_FOR_Y;

for(double dX=LEFT_MARGINE_FOR_X; dX<=RIGHT_MARGINE_FOR_X; dX+=1.0)
  for(double dY=LEFT_MARGINE_FOR_Y; dY<=RIGHT_MARGINE_FOR_Y; dY+=1.0)
    if( dMaximumValue<((dX*dX+dY*dY)/(dY*dY+dB)))
    {
      dMaximumValue=((dX*dX+dY*dY)/(dY*dY+dB));
      dMaximumX=dX;
      dMaximumY=dY;
    }

cout<<"Maximum value of the function is="<< dMaximumValue<<endl;
cout<<endl<<endl;
cout<<"Value for x="<<dMaximumX<<endl
    <<"Value for y="<<dMaximumY<<endl;

	return EXIT_SUCCESS;
}

现在,如果我们更仔细地分析代码,我们会注意到 dX*dX 部分的计算次数比应有的要多,在这种情况下计算了 200 次,这是 CPU 时间。我们能做什么?

诀窍之一是创建一个变量 dX_Squer = dX*dX,并在第一次重复之后计算,然后我们可以在之后的所有计算中使用它。您只需要再添加一个括号。

您可以在上面的代码中进行更多优化,只需尝试发现它们即可。

我们可以考虑的下一点是我们的算法有多普遍,以及从速度的角度来看它有多优化。

在这种情况下,我们可以根据输入集的大小应用很少的算法。我们的意思是什么?

例如,在我们之前的一篇 c++ 文章中,我们讨论了在许多零中只有两个 1 的二进制数

我们可以使用 MVA 算法,从速度的角度来看,它可以在较小的数字上优于原始算法,这些数字适合 unsigned long long int,但是如果您将我的算法与向量结合使用,它可以用于您尝试的某些问题选择集合中的两个对象。

因此,为了创建可能的最佳解决方案,您可以合并两种算法并根据问题的大小应用一种。因此,如果使用的数字小于 unsigned long long int,您可以使用第一种算法,如果数字不适合已经提到的数据类型,您可以使用向量或其他一些数据结构。

与此类似的是数字的加法,其中考虑 long long int 的情况很简单,但是如果我们需要添加大小比 unsigned long long int 大得多的大数字,您可以使用向量来存储它们和用你的算法应用加法运算。如果你更喜欢类,你可以使用它们,但如果你不需要 OOP 方法,你可以只使用双链表或数组或其他一些更合适的数据结构。

2. 优化内存代码

现在我们将看看如何从内存消耗的角度优化代码。

让我们举一个简单的例子。让我们尝试交换内存中的两个值,这在许多排序算法中都是这样做的。

有些人喜欢把这看作是两个人坐在两把椅子上,并在交换过程中再增加一把椅子作为其中一个的临时支架。

int nFirstOne =1, nSecondOne=2;
int nTemp = nFirstOne;
nFirstOne = nSecondOne;
nSecondOne = nTemp;

这很好。但是使用 nTemp 在内存中保留了一些将用于复制一个变量的地方。

这可以在没有 nTemp 的情况下完成,如下所示:

int nFirsOne = 3, nSecondOne = 7;
nFirstOne += nSecondOne;
nSecondOne = nFirstOne ? nSecondOne;
nFirstOne -= nSecondOne;

在某些情况下,您可能在内存中有需要交换位置的大对象。那么,你能做什么?您可以使用它们的地址而不是处理许多内存位置,而不是替换所有内存位置,您只需更改它们的地址。

您如何知道您的代码是否更快以及如何计算?

好吧,当你完成你的代码时,它会在一些汇编程序中转换,然后转换成称为机器代码的东西。每个操作都在处理器或计算机的其他部分(如数学协处理器)或图形卡或类似的东西中执行。

一个操作可以在一个或几个时钟周期内完成,这就是为什么乘法比除法更快的原因,但是您是否选择了由编译器完成的一些优化也很重要。

有时,优化的任务可以留给编译器。对于所有可用的 C++ 编译器,请检查此GCC 优化选项

要了解程序的速度,您应该了解正在使用的设备的架构。有时事情会变得更快,因为您的程序在高速缓存中,或者您使用数学协处理器,或者因为大多数时候分支预测器都正确。

现在让我们考虑这些数字 O(n), O(log(n) *n), n*n, n!。要根据输入集的大小估计算法,请使用此数字。

如果你有一个大小为 n 的算法并且你输入 10 个元素,你得到时间 t,如果你输入 100 个元素,你最终会得到比 t 长 10 倍的时间。如果您处理一个等于 n*n 的程序,并且将集合的大小从 10 增加到 100,则程序不会慢 10 倍,而是大约 10*10 倍。您应该了解数字可能对您的算法产生的这些类型的限制。

有些人认为他们可以对代码进行计时,并且很清楚算法的速度。好吧,让我们想想。您编写的大多数程序都不是内核模式,这意味着它们可能会被操作系统停止,并且处理器可能会被分配给另一个任务等等。这意味着您的程序将多次停止和启动。如果您的内核甚至处理器很少,那么弄清楚编程会发生什么可能会更加困难。

测量算法速度的想法非常不确定。嗯,结果就像北极上的雪花或沙漠中的沙子一样有用。

唯一好的结果是,如果您找到一种方法来防止您的程序丢失他所在的核心,或者可能停止计时然后继续,但是您需要消除每次停止时都会添加的中断时间程序,以及启动初始化。

您还会注意到一些差异,因为如果您应用不同的优化,相同的代码将不会转换为机器代码,并且您应该知道,顺便说一下,已经有一个产品可以以与其他版本不同的方式翻译代码它执行什么架构也很重要,也取决于安装的内存量、高速缓存、预测方法等。

3. printf 和 scanf 与 cout 和 cin

有时,如果您对同一任务使用不同的函数,您将获得更快的代码。

前两个函数主要用于 C 风格的编程,但有时您可以将它用于文件操作,速度上的微小差异可以节省很多时间。

例如,让我们假设您要读取文件中的数字。
从安全角度来看, cout 和 cin 将被视为文件的更好选择,因为您将在 fstream 标头中有足够的说明。

如果您在 C++ 中使用 C 或 printf,您应该考虑其他一些可以进一步提高程序速度的函数。

对于字符串,您可以使用 puts、gets 或它们的等效项进行文件操作。好吧,它们没有格式化,以一种方式写入数据需要一些时间。

4. 使用运算符

大多数基本操作(如 +=、-= 和 *=)在应用于基本数据类型时也会减慢程序速度。为了确保您需要知道它是如何在您的计算机上转换为汇编程序的。

一个有趣的想法是用它们的前缀版本替换后缀增量和减量。

有时你可以尝试使用运算符 >> 或 << 代替乘法或除法,但要非常小心,你可能会以这种方式结束错误,然后修复它,你可以添加一些范围估计,这会变慢不是您开始使用的原始代码。

位运算符和伴随它们的技巧可以提高程序的速度,但你应该非常小心,因为你最终可能会得到依赖于机器的代码,这是需要避免的。可以肯定的是,您仍然可以在 C++ 中使用汇编器中的 add move 进行编码。

理解这是一种混合语言很重要,它将支持汇编编码、面向问题的解决方案、面向对象的解决方案,如果您添加一些额外的库,您可以使用一些不常用的更高级的技巧。

5.如果条件优化

如果您在代码中使用 if,在可能的情况下,最好将 if 替换为 switch。在“if”中,您通常会进行测试,这可能会产生稍慢的代码。

了解 if 命令的一个好事实是知道它内置了一些优化。好吧,如果您几乎没有与 && 或 || 相关联的条件 无需计算完整表达式就可以评估这是真还是假。

让我们用与 && 运算符连接的两个条件来说明这一点。如果您有表达式 p 和 q,一旦 p 等于 false,您就会知道结果无法得到 true,这在 C/C++ 中使用,有时这可能是人们得到错误代码的原因.

如果您在某些情况下可以说某事可能会更频繁地发生,请将其放在前面,因为更有可能说该表达是错误的或正确的。如果您有许多条件要计算,并且可以对它们进行排序,请考虑先将该范围拆分为几个子范围。

可能发生的一件坏事是,您创建了永远不会使用的分支,甚至可以添加几行代码,而您永远不会使用这些情况。

有时你会有一个由许多条件组成的很长的表达式,一个可以使用返回真或假的函数,但是函数很昂贵,它们使用堆栈并且可以创建很少的副本,如果可能的话,你可以使用宏或带有一个变量来提高速度并创建更易于维护的代码。

另外,不要忘记否定也是一种操作。

6. 函数问题

在使用函数时,如果您不小心,最终可能会创建错误的代码。

例如,如果您有这样的代码,那可能是件坏事。

for(int i=1; i<=10; ++i)
     DoSomething(i);

为什么?一旦你编写了这样的代码,你就必须调用 DoSomething 10 次,而且我们已经提到函数调用可能很昂贵。

为了更好地实现这一点,您可以这样做,并在您的函数中实现重复。

DoSomething(n);

接下来要考虑的是内联函数。如果它们很小,则有可能像宏一样使用它们。通过这种方式,您可以从速度、更好的组织以及可重用性中受益。

将大对象传递给函数时,可以使用指针或引用。更喜欢使用引用,因为它们会创建更易于阅读的代码。

如果您不担心更改传递给函数的值,请使用引用。如果您使用常量对象,则使用 const 可能会很有用,这将节省一些时间。

当您使用支持 C99 标准的 C 时,您可以选择对指向的指针使用限制。

在某些情况下,函数中的强制转换可能会提高代码的速度。您应该根据您的具体情况考虑这一点。

在函数中创建临时对象可能会减慢程序的速度。我已经展示了在某些情况下如何避免使用临时变量。

此外,虽然递归在某些特定场景中非常有用,但通常它会生成执行缓慢的代码。如果可能,尽量避免递归,当您不需要使用它来解决您的问题时。

7. 优化循环

如果您想检查一个数字是否小于 10 或大于零,请选择第二个选项。

测试某事物是否等于 0 比比较两个不同的数字要快。

换句话说,与下面显示的替代选项相比,以下内容更慢:

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

与上面的 for 循环相比,以下更快。但是,对于初学者来说,这可能更难阅读。

for(i=10; i--; )

与这种情况类似,如果您处于可以选择表单 !=0 和 <=n 的情况下,使用第一个会更快。例如,当您尝试在单独的函数中计算阶乘时。

在您使用不同参数(范围从 1 到 5)调用的函数很少的情况下,最好避免循环,最好使用具有 5 个调用的线性调用。

如果您要使用的情况是:一个循环和几个任务或几个循环每个循环中一个任务。选择第一个选项。这是一个可以生成更快代码的技巧。我不确定,但编译器可能仍然无法优化它。

8. 数据结构优化

我们使用的数据结构是否会影响代码的性能?

这个问题的答案并不简单,你可以从简单的数学中得到答案。它相当模糊,难以表述。

为了说明我的说法,我们将分析一个例子。如果您的任务是创建如下所示的排列,那么您可以使用数组或链表。

1, 2, 3, 4,
2, 3, 4, 1,
3, 4, 1, 2,
4, 1, 2, 3,

如果使用数组,则可以复制第一个元素并将所有其他元素移向第一个元素,然后将第一个元素移动到最后一个位置。这会创建很多不需要的操作,以至于您的程序或函数会非常缓慢。

如果您将数据保留在列表中,您可以很容易地创建一个程序,该程序将优于我们提到的数组。

有时,如果您以某种形式的树保存数据,您可以创建比没有足够数据结构的程序执行得更快的程序。

使用数据结构时要小心。有时可以在不保留数组的所有元素或根本不使用任何数据结构的情况下解决问题。

要详细说明这个主题,请参阅我们对Fibonacci 算法的讨论。如果您查看 Fibonacci 元素,您可能会在将向量与递归结合使用时被欺骗,但您可以使用应用数学中的一些技巧来创建非常快速的代码。

9. 二分搜索或顺序搜索

我们应该使用二分搜索还是顺序搜索来解决问题?

我们在编程时需要做的一项常见任务是在某些数据结构中搜索某个值。是的,它是哈希表、多级哈希表等的基础。

如果您试图在一组数字中找到一个数字,您可以有两种策略。

第一个策略非常简单。你有你正在寻找的数组和值。从数组的开头开始查找值,如果找到就停止搜索,如果没有找到值,您将位于数组的末尾。这种策略有很多改进。

第二种策略需要对数组进行排序。如果数组未排序,您将不会得到您想要的结果。如果数组已排序,则将其分成两半。前半部分数组的元素小于中间的元素,另一半数组的元素大于中间的元素。如果您遇到两个标记的位置不正确的情况,您应该知道您没有一直在寻找的价值。

这里的困境是什么?如果您对数组元素进行排序,您将损失一些时间,但如果您对此进行投资,您可以从更快的二分搜索中受益。

这是您需要很好地理解问题并根据您的特定场景根据最佳情况采取行动的情况之一。

10. 优化数组

数组是最基本的数据结构之一,它的元素在内存中占据一些空间。

要了解这些优化的工作原理,您应该了解数组结构。好的,我这是什么意思。数组的名称是一个常量指针,指向数组的第一个元素。这意味着您可以使用指针和指针算法。

如果您像这样访问数组成员:

for(int i=0; i<n; i++) nArray[i]=nSomeValue;

代替上面的代码,下面的代码更好:

for(int* ptrInt = nArray; ptrInt< nArray+n; ptrInt++) *ptrInt=nSomeValue;

原因在于使用指针的操作。在上面的例子中,我们有指向 int 数据类型的指针,它从数组的名称中获取地址。在这种情况下,它是 nArray,我们增加一个元素的地址,指针移向数组的末尾以获取 int 数据类型的大小。


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。