您使用什么简单的技术来提高性能?


21

我说的是我们编写简单例程的方式,目的是在不使您的代码更难阅读的情况下提高性能,例如,这是我们学习到的典型方法:

for(int i = 0; i < collection.length(); i++ ){
   // stuff here
}

但是,我通常在a foreach不适用的情况下执行此操作:

for(int i = 0, j = collection.length(); i < j; i++ ){
   // stuff here
}

我认为这是一种更好的方法,因为它只会调用length一次该方法……我的女友说这虽然很神秘。您还可以在自己的开发中使用其他任何简单的技巧吗?


34
+1是为了让女友在不清楚您的代码时告诉您。
克里斯多

76
您刚刚发布此信息是为了告诉我们您有女朋友。
乔什·K

11
@Christian:不要忘记有编译器优化可能会为您完成此操作,因此您可能只会影响可读性,而根本不会影响性能。过早的优化是万恶之源...避免在同一行上避免多个声明或赋值,不要让人们读过两次...您应该使用常规方式(您的第一个示例)或将for循环外的第二个声明(尽管这也会降低可读性,因为您需要回读以了解j的含义)。
Tamara Wijsman 2010年

5
@TomWij:正确(完整)的语录: “我们应该忘记效率低下的问题,大约有97%的时间是这样:过早的优化是万恶之源。但是,我们不应该在这3%的临界值中放弃机会。 ”
罗伯特·哈维

3
@tomwij:如果您花了这三个百分点,那么按照定义,您应该使用对时间要求严格的代码,而不要浪费时间在其他97%上。
罗伯特·哈维

Answers:


28

插入过早的讨论是万恶之源

也就是说,这是我为了避免不必要的效率而习惯的一些习惯,在某些情况下,还使我的代码更简单,更正确。

这不是讨论一般原则,而是要注意一些事情,以避免在代码中引入不必要的低效率。

了解你的大O

这可能应该合并到上面冗长的讨论中。在内部循环重复计算的循环内部循环会变慢,这是非常普遍的常识。例如:

for (i = 0; i < strlen(str); i++) {
    ...
}

如果字符串确实很长,这将花费大量时间,因为在每次循环迭代时都会重新计算长度。请注意,GCC实际上会优化这种情况,因为它strlen()被标记为纯函数。

在对一百万个32位整数进行排序时冒泡排序是错误的方法。通常,排序可以在O(n * log n)的时间内完成(或者在基数排序的情况下更好),因此,除非您知道数据会很小,否则请寻找至少为O(n *登录n)。

同样,在处理数据库时,请注意索引。如果您SELECT * FROM people WHERE age = 20没有人的索引,则需要O(n)顺序扫描,而不是更快的O(log n)索引扫描。

整数算术层次

使用C进行编程时,请记住,某些算术运算比其他算术运算更昂贵。对于整数,层次结构如下所示(首先是最便宜的):

  • + - ~ & | ^
  • << >>
  • *
  • /

当然,编译器会像通常优化的东西n / 2,以n >> 1自动,如果您定位的是主流电脑,但如果你的目标嵌入式设备,你可能无法得到这样的奢侈。

并且,% 2并且& 1具有不同的语义。除法和模数通常四舍五入为零,但是它是由实现定义的。很好,>>并且&总是四舍五入为负无穷大,(我认为)这更有意义。例如,在我的计算机上:

printf("%d\n", -1 % 2); // -1 (maybe)
printf("%d\n", -1 & 1); // 1

因此,请使用有意义的内容。别以为% 2当初写作时会用自己做一个好男孩& 1

昂贵的浮点运算

避免重浮点运算像pow(),并log()在代码中并不真正需要他们,整数时尤其如此。以读取数字为例:

int parseInt(const char *str)
{
    const char *p;
    int         digits;
    int         number;
    int         position;

    // Count the number of digits
    for (p = str; isdigit(*p); p++)
        {}
    digits = p - str;

    // Sum the digits, multiplying them by their respective power of 10.
    number = 0;
    position = digits - 1;
    for (p = str; isdigit(*p); p++, position--)
        number += (*p - '0') * pow(10, position);

    return number;
}

这种使用pow()(以及使用它所需的int<-> double转换)不仅非常昂贵,而且还为精度损失创造了机会(顺便说一句,上面的代码没有精度问题)。这就是为什么我在非数学环境中看到这种类型的函数时会畏缩的原因。

此外,请注意,下面的“聪明”算法在每次迭代中都乘以10,实际上比上面的代码更简洁:

int parseInt(const char *str)
{
    const char *p;
    int         number;

    number = 0;
    for (p = str; isdigit(*p); p++) {
        number *= 10;
        number += *p - '0';
    }

    return number;
}

非常彻底的答案。
Paddyslacker

1
注意,过早的优化讨论不适用于垃圾代码。首先,您应该始终使用运行良好的实现。

请注意,GCC实际上优化了这种情况,因为strlen()被标记为纯函数。 我认为您的意思是它是一个const函数,而不是纯函数。
安迪·莱斯特

@安迪·莱斯特(Andy Lester):实际上,我的意思是纯洁的。 在GCC文档中,它指出const比纯函数稍微严格一点,因为const函数无法读取全局内存。 strlen()检查其指针参数指向的字符串,这意味着它不能为const。此外,strlen()在glibc中确实标记为纯string.h
Joey Adams

你是对的,我的错,我应该仔细检查一下。由于两者之间的细微差别,我一直在将Parrot项目注释为pureconst,甚至将其记录在头文件中。docs.parrot.org/parrot/1.3.0/html/docs/dev/c_functions.pod.html
Andy Lester 2010年

13

从您的问题和注释线程看来,您似乎“认为”此代码更改可以提高性能,但是您实际上并不知道它是否可以这样做。

我是肯特·贝克Kent Beck)哲学的粉丝:

“使它起作用,使其正确,使其快速。”

我提高代码性能的技术是,首先使代码通过单元测试并经过合理的分解,然后(特别是对于循环操作)编写一个单元测试,以检查性能,然后重构代码或考虑使用其他算法,如果您选择的无法正常工作。

例如,为了测试.NET代码的速度,我使用NUnit的Timeout属性编写断言,即对特定方法的调用将在一定时间内执行。

使用类似NUnit的timeout属性的代码示例(以及循环的大量迭代),您实际上可以证明对代码的“改进”是否确实有助于改善该循环的性能。

一个免责声明:尽管这在“微型”级别上有效,但它当然不是测试性能的唯一方法,也没有考虑到在“宏观”级别上可能出现的问题-但这是一个好的开始。


2
尽管我坚信分析,但我也谨记Cristian在寻找的技巧是明智的。我将始终选择两种同样易读的方法中的较快方法。被迫进行成熟后优化是没有意思的。
AShelly

不一定需要进行单元测试,但是花这20分钟来检查某些性能神话是否成立总是值得的,尤其是因为答案通常取决于编译器以及-O和-g标志的状态(或Debug /如果是VS,则发布。
mb10

+1此答案是我对问题本身的相关评论的补充。
Tamara Wijsman 2010年

1
@AShelly:如果我们正在谈论循环语法的简单重构,那么在事实发生之后对其进行更改非常容易。同样,您发现同样可读的内容对于其他程序员而言可能并非如此。最好尽可能使用“标准”语法,并且仅在必要时才对其进行更改。
Joeri Sebrechts

@AShelly可以肯定,如果您可以想到两种可读性相同的方法,并且选择效率较低的方法,而您只是没有做好工作?有人会这样做吗?
glenatron 2010年

11

请记住,编译器可能会:

for(int i = 0; i < collection.length(); i++ ){
   // stuff here
}

变成:

int j = collection.length();
for(int i = 0; i < j; i++ ){
   // stuff here
}

或类似的内容(如果collection循环中未更改)。

如果此代码在应用程序的时间紧迫部分中,则值得查明是否是这种情况-甚至确实可以更改编译器选项来执行此操作。

这将保持代码的可读性(就像大多数人希望看到的那样),同时又为您节省了很少的额外机器周期。然后,您可以自由地专注于编译器无法帮助您的其他领域。

附带说明一下:如果您collection通过添加或删除元素来在循环内进行更改(是的,我知道这是一个坏主意,但是确实发生了),那么您的第二个示例要么不会遍历所有元素,要么将尝试访问过去数组的末尾。


1
为什么不明确地做呢?

3
在某些进行边界检查的语言中,如果您明确进行操作,则会使代码运行缓慢。通过循环到collection.length,编译器会为您将其移出并忽略边界检查。通过从应用程序其他位置循环访问某个常量,您将在每次迭代中进行边界检查。这就是测量很重要的原因-对性能的直觉几乎永远都不对。
凯特·格雷戈里

1
这就是为什么我说“值得一查”。
克里斯·

C#编译器如何才能像stack.pop()那样知道collection.length()不会修改collection?我认为最好检查IL,而不要假设编译器对此进行了优化。在C ++中,可以将方法标记为const(“不更改对象”),因此编译器可以安全地进行此优化。
JBRWilkinson

1
这样做的@JBRW优化器也知道可以正常调用集合的方法,尽管这不是C ++。毕竟,您只能进行边界检查,是否可以注意到某物是一个集合并且知道如何获取其长度。
凯特·格雷戈里

9

通常不建议这种优化。编译器可以轻松完成这一优化工作,因为您使用的是更高级别的编程语言,而不是汇编语言,因此请在同一级别进行思考。


1
给她一本关于编程的书;)
Joeri Sebrechts 2010年

1
+1,因为我们的大多数女友可能对Lady Gaga的兴趣要大于对代码的清晰度。
haploid

您能解释为什么不建议这样做吗?
Macneil

@macneil很好...这种技巧使代码变得不那么普遍并且完全不起作用,该优化应该由编译器完成。
tactoth

@macneil如果您使用的是更高级别的语言,请以同一级别进行思考。
tactoth

3

这可能不适用于通用编码,但是最近我主要从事嵌入式开发。我们有一个特定的目标处理器(它不会变得更快-当他们在20多年后退休该系统时,它似乎已经过时了),而且很多代码的时间限制都非常严格。像所有处理器一样,该处理器对于某些操作是快还是慢也有一些怪癖。

我们使用一种技术来确保我们生成最有效的代码,同时保持整个团队的可读性。在那些最自然的语言构造无法生成最有效代码的地方,我们创建了一个宏,可以确保使用最佳代码。如果我们为其他处理器执行后续项目,则可以为该处理器上的最佳方法更新宏。

作为一个特定的例子,对于我们当前的处理器,分支清空了管道,使处理器停滞了8个周期。编译器采用以下代码:

 bool isReady = (value > TriggerLevel);

并把它变成相当于

isReady = 0
if (value > TriggerLevel)
{
  isReady = 1;
}

这将花费3个周期,如果跳过则将花费10个周期isReady=1;。但是处理器具有单周期max指令,因此最好编写代码来生成此序列,以确保始终花费3个周期:

diff = value-TriggerLevel;
diff = max(diff, 0);
isReady = min(1,diff);

显然,这里的意图不如最初的明确。因此,我们创建了一个宏,在需要布尔型“大于”的比较时就可以使用该宏:

#define BOOL_GT(a,b) min(max((a)-(b),0),1)

//isReady = value > TriggerLevel;
isReady = BOOL_GT(value, TriggerLevel);

我们可以为其他比较做类似的事情。对于局外人来说,与仅使用自然构造的代码相比,该代码的可读性较差。但是,花一点时间处理代码后,它很快就会变得很清楚,并且比让每个程序员尝试自己的优化技术要好得多。


3

好吧,第一个建议是避免这种过早的优化,直到您完全了解代码发生了什么,以便确保实际上使它变快而不是变慢。

例如,在C#中,如果您循环数组的长度,则编译器将优化代码,因为它知道访问数组时不必对索引进行范围检查。如果尝试通过将数组长度放在变量中来优化它,则会破坏循环与数组之间的连接,并实际上使代码变慢。

如果要进行微优化,则应将自己限制在使用大量资源的已知事物上。如果只是稍微提高了性能,则应改用最易读和可维护的代码。随着时间的流逝,计算机的工作方式会发生变化,因此您发现的速度现在可能会稍快一些,可能就不会这样。


3

我有一个非常简单的技术。

  1. 我使我的代码工作。
  2. 我测试它的速度。
  3. 如果速度很快,我将返回第1步以使用其他功能。如果速度很慢,我将其剖析以查找瓶颈。
  4. 我解决了瓶颈。返回步骤1。

在很多情况下,它可以节省时间来规避此过程,但是通常您会知道是这种情况。如有疑问,我默认情况下会坚持使用。


2

利用短路的优势:

if(someVar || SomeMethod())

花费的时间与编写代码的时间一样长,并且可读性如下:

if(someMethod() || someVar)

但是随着时间的流逝,它的评估速度会更快。


1

等待六个月,让老板给大家买新计算机。说真的 从长远来看,程序员的时间要比硬件贵得多。高性能计算机允许编码人员以简单的方式编写代码,而无需担心速度。


6
嗯...您的客户看到的性能如何?您是否也足够有钱为他们购买新计算机?
罗伯特·哈维

2
而且我们几乎已经击中了性能壁垒;多核计算是唯一的出路,但是等待不会使您的程序使用它。
mbq 2010年

+1此答案是我对问题本身的相关评论的补充。
Tamara Wijsman 2010年

3
当您拥有成千上万的用户时,没有比硬件更昂贵的编程时间了。程序员的时间并不比用户的时间重要,请尽快解决这个问题。
HLGEM 2010年

1
养成良好的习惯,那么您就不需要花任何时间,因为这是您一直在做的事情。
Dominique McDonnell

1

尽量不要在优化之前进行过多优化,然后在进行优化时就不必担心可读性。

除了讨厌不必要的复杂性之外,我几乎讨厌什么,但是当您遇到复杂的情况时,通常需要复杂的解决方案。

如果您以最明显的方式编写代码,那么请进行注释,以解释进行复杂更改时为什么更改了代码。

但是,根据您的具体意思,我发现很多时候使用与默认方法相反的布尔值有时会有所帮助:

for(int i = 0, j = collection.length(); i < j; i++ ){
// stuff here
}

可以变成

for(int i = collection.length(); i > 0; i-=1 ){
// stuff here
}

在许多语言中,只要您对“填充”部分进行适当的调整并且它仍然可读。它只是没有像大多数人想的那样处理问题,因为它倒数。

在c#中,例如:

        string[] collection = {"a","b"};

        string result = "";

        for (int i = 0, j = collection.Count() - 1; i < j; i++)
        {
            result += collection[i] + "~";
        }

也可以写成:

        for (int i = collection.Count() - 1; i > 0; i -= 1)
        {
            result = collection[i] + "~" + result;
        }

(是的,您应该使用一个join或一个stringbuilder来实现,但是我试图做一个简单的例子)

还有许多其他技巧并不难遵循,但其中许多技巧并不适用于所有语言,例如在旧vb中使用作业左侧的中部来避免字符串重新分配的代价或以二进制模式读取文本文件当文件太大而无法进行readtoend时,.net中的命令将无法通过缓冲惩罚。

我能想到的唯一真正通用的情况是,将布尔布尔代数应用于复杂的条件,以尝试将方程式转换为更有可能利用短路条件或使复杂的条件变为可能的情况。一组嵌套的if-then或case语句完全包含在一个方程式中。这些都不在所有情况下都有效,但是它们可以节省大量时间。


这是一个解决方案,但是编译器可能会发出警告,因为对于大多数常见类,length()返回的是无符号类型
stijn 2010年

但是通过反转索引,迭代本身可能会变得更加复杂。
Tamara Wijsman 2010年

@stijn我在写c#时就想到了c#,但是出于这个原因,也许这个建议也属于特定于语言的类别-参见edit ... @ToWij当然,我不认为有任何这种性质的建议那没有冒那个的风险。如果您的// stuff是某种堆栈操作,它甚至可能无法正确地逆转逻辑,但是在许多情况下,恕我直言,在大多数情况下,这样做是正确的,也不会太令人困惑。
条例草案

你是对的; 在C ++中,我仍然希望使用'normal'循环,但要从迭代中取出length()调用(如const size_t len = collection.length(); for(size_t i = 0; i <len; ++ i){})有两个原因:我发现“正常”前向计数循环更易读/易懂(但这可能只是因为它更常见),并且它使循环不变的length()调用退出了循环。
stijn 2010年

1
  1. 轮廓。我们甚至有问题吗?哪里?
  2. 在90%与IO相关的情况下,请应用缓存(并可能获得更多的内存)
  3. 如果与CPU相关,请应用缓存
  4. 如果性能仍然是一个问题,我们就离开了简单技术的领域-做数学。

1

使用可以找到的最佳工具 -好的编译器,好的Profiler,好的库。选择正确的算法,或者更好的选择-使用正确的库为您完成。琐碎的循环优化工作不多,而且您不如优化编译器那么聪明。


1

对我来说,最简单的方法是,只要普通情况下的使用模式适合[0,64)的范围,但在极少数情况下上限不小的情况下,就使用堆栈。

简单的C示例(之前):

void some_hotspot_called_in_big_loops(int n, ...)
{
    // 'n' is, 99% of the time, <= 64.
    int* values = calloc(n, sizeof(int));

    // do stuff with values
    ...
    free(values);
}

之后:

void some_hotspot_called_in_big_loops(int n, ...)
{
    // 'n' is, 99% of the time, <= 64.
    int values_mem[64] = {0}
    int* values = (n <= 64) ? values_mem: calloc(n, sizeof(int));

    // do stuff with values
    ...
    if (values != values_mem)
        free(values);
}

我之所以这样概括,是因为此类热点在分析中大量出现:

void some_hotspot_called_in_big_loops(int n, ...)
{
    // 'n' is, 99% of the time, <= 64.
    MemFast values_mem;
    int* values = mf_calloc(&values_mem, n, sizeof(int));

    // do stuff with values
    ...

    mf_free(&values_mem);
}

在这99.9%的情况下,当分配的数据足够小时,以上将使用堆栈,否则将使用堆。

在C ++中,我使用符合标准的小序列对此进行了概括(类似于 SmallVector围绕实现)。

这不是史诗般的优化(我将操作完成的时间从3秒减少到1.8秒),但是应用起来却需要微不足道的努力。通过引入一行代码并更改两个代码,您可以将时间从3秒降低到1.8秒,对于这么小的钱,这是一个不错的选择。


0

在访问数据时,您可以进行很多性能更改,这些更改将对您的应用程序产生巨大影响。如果编写查询或使用ORM访问数据库,则需要阅读一些有关所使用的数据库后端的性能调优书。您可能正在使用性能不佳的已知技术。除了无知,没有其他理由。这不是过早的优化(我诅咒说这个的人,因为它被广泛地使用了,从不担心性能),这是一个很好的设计。

只是SQL Server性能增强器的快速示例:使用适当的索引,避免使用游标-使用基于集合的逻辑,使用可调整的where子句,不要将视图堆积在视图之上,不要返回比您需要或更多的数据不需要的列,请不要使用相关子查询。


0

如果是C ++,则应养成++i而不是的习惯i++++i永远不会更糟,它的含义与独立语句完全相同,并且在某些情况下可能会提高性能。

偶尔更改现有代码将不会有所帮助,但这是一个很好的习惯。


0

我对此有一些不同的看法。只是简单地遵循您在这里获得的建议并不会带来太大的变化,因为您需要犯一些错误,然后需要解决这些错误,然后需要向他们学习。

您需要犯的错误是按照每个人的方式设计数据结构。也就是说,具有冗余数据和许多抽象层,具有在整个结构中传播的属性和通知,试图使其保持一致。

然后,您需要进行性能调整(性能分析),并向您展示在许多方面如何使您花费数十亿美元的周期是很多抽象层,其中,属性和通知会在整个结构中传播,以使其保持一致。

您可能无需对代码进行重大更改就可以解决这些问题。

然后,如果幸运的话,您将了解到较少的数据结构会更好,并且能够容忍暂时的不一致比尝试使许多事情与消息浪潮保持紧密一致更好。

编写循环的方式实际上与它无关。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.