是什么影响了代码的速度?

时间:2009-03-06 20:19:26

标签: profiling performance

查看代码运行速度的方法是性能分析。有它的工具等,但我想知道代码速度的因素是什么。

例如,我被告知图像编辑软件将使用按位运算而不是整数变量来计算它们的内容,因为它更快。

因此,与binairy相比,这必须意味着使用整数和其他原始类型需要更多的步骤来计算。

必须有其他的东西,但我没有足够的经验来了解操作系统如何连接到您的硬件和许多编码语言的内部工作以了解什么。

所以我在这里问:你知道是什么影响了代码的速度吗?

必然是程序的速度。

12 个答案:

答案 0 :(得分:22)

整数二进制文件。或者更确切地说,整数只是整数,可以用任意数字表示。在基数10中,您将写入13,在基数16中,您将写入d(或0xd),以二进制形式写入1101(或0b1101)。罗马人会写XIII。它们都代表数字“十三”的相同概念。在人类中,我们倾向于使用基数10表示,但是当您要求计算机处理整数时,它使用二进制表示。它并没有什么不同。无论我怎么写,十三加四十五都会得到相同的结果。 XIII + 0x2D = 13 + 45 = 0xd + 0b101101。使用哪种表示无关紧要,算术运算的结果是相同的。这就是为什么我们允许CPU使用二进制表示来进行所有整数处理。

某些编程语言也提供了“十进制”数据类型,但这通常与浮点算术相关,其中并非所有值都可以在所有基数中表示(1/3可以在基数3中轻松表示,但不能在例如,在图2或10中,1/10可以用基数10表示,但不能用2)

表示

但是,将任何特定操作单独列为“慢”非常困难,因为取决于。现代CPU采用了大量技巧和优化来大多数时间加速大多数操作。实际上,要获得高效代码需要做的就是避免所有特殊情况。并且有很多这些,并且它们通常更多地与指令的组合(和顺序)相关,而不是使用指令。

为了让您了解我们所讨论的细微差别,浮点运算可以在理想情况下以尽可能快(或有时快于)整数算术的方式执行,但延迟时间更长,意味着理想的性能更难实现。否则几乎免费的分支会变得很痛苦,因为它们会抑制指令重新排序和调度,在编译器中以及在CPU上动态执行,这使得隐藏此延迟变得更加困难。 延迟定义从指令启动到结果准备所需的时间;大多数指令只占用CPU一个时钟周期即使结果尚未准备好以下周期,CPU也能够启动另一条指令。这意味着如果不立即需要结果,高延迟指令几乎是免费的。但是如果你需要将结果提供给下一条指令,那么就必须等到结果完成。

无论你做什么,某些指令都很慢,并且通常会停止CPU的相关部分,直到指令完成为止(平方根是常见的例子,但整数除法可能是另一个。在某些CPU上,加倍)一般遭受同样的问题) - 另一方面,虽然浮点平方根将阻止FP管道,但它不会阻止您同时执行整数指令。

有时,将值存储在可根据需要重新计算的变量中会更快,因为它们可以放在寄存器中,从而节省几个周期。其他时候,它会因为你的寄存器用完而变慢,并且必须将值推送到缓存,甚至推送到RAM,这使得每次使用都需要重新计算。您遍历内存的顺序会产生巨大的差异。随机(分散)访问可能需要数百个周期才能完成,但顺序访问几乎是即时的。以正确的模式执行读/写操作允许CPU几乎一直在缓存中保存所需的数据,而通常“正确的模式”意味着按顺序读取数据,并处理~64kb的块一次。但有时候不是。 在x86 CPU上,一些指令占用1个字节,其他指令取17个。如果你的代码包含很多前者,指令获取和解码不会成为瓶颈,但如果它充满了更长的指令,那么可能就是限制每个周期CPU可以加载多少指令,然后它能够​​执行的数量无关紧要。

现代CPU中的性能很少有通用规则。

答案 1 :(得分:10)

我觉得你错了。整数二进制数。图像编辑软件将尽一切可能避免浮点计算,因为与整数或位移操作相比,它们速度极慢。

但通常情况下,首先要通过选择正确的算法进行优化,而不是通过担心是后期增量还是预增量等小问题进行优化。

例如:我过去两天只是加快了重新计算一组特定值的方式。我从循环中抽出一些东西并预先计算它,所以它只进行了M次而不是M次N次,并且在变量中存储了一个值而不是每次从其他地方查找它因为使用了该值在比较器中,所以在Collections.sort阶段会调用它。我的总执行时间从大约45秒到20秒。然后我的一位同事在这里待了很长时间,指出我不需要重新计算这些值,我可以将它们从不同的物体中拉出来。突然间它会在2秒内执行。现在 是我可以信赖的优化。

答案 2 :(得分:4)

“程序的速度”通常归结为算法选择。错误的算法可以将2秒的任务变为2分钟或更差。当你专注于做到这一点时,你会看到最佳的性能提升。

一旦你有一个好的算法,你可能仍然需要一个更有效的实现。实现这一点通常依赖于“代码速度”类型选择。有几件事需要考虑,往往与硬件有关。对一个处理器的优化实际上可能会减慢其他处理器上的代码。

一些“代码速度”因素:

  • 整数与浮点数
  • 分支,函数调用,上下文切换的成本
  • CPU指令管道中断
  • 维护内存访问的位置,缓存一致性。

答案 3 :(得分:4)

为了扩展Oscar Reyes所说的内容,即使在最低级别,数据传输实际上也是性能的关键因素,因此不仅数量而且CPU执行的操作类型对整体性能至关重要。 / p>

对于像x86这样的CISC处理器尤其如此,其中指令本身可能具有不同的周期数,尽管现代设计大多可以缓解这种情况。但是,在内存加载和存储操作的情况下,所有CPU都是如此,这可能比任何其他操作贵很多很多倍。利用现代CPU的时钟速率与内存延迟,您可以看到,如果出现页面错误并且必须转出磁盘,则在数百万次缓存未命中的情况下可能浪费了数十条指令。在许多情况下,当考虑缓存行为和加载和存储行为时,执行大量算术运算以重新计算值而不是缓存它并从内存重新读取它可能实际上要快得多,即使加载是一个汇编指令与许多用于执行计算的指令。在某些情况下,将您的表现仅限于加载和存储并将其他操作视为“免费”可能实际上是有意义的,尽管这自然取决于具体情况。

答案 4 :(得分:4)

代码速度主要受计算机体系结构的低级优化的影响,无论是在CPU还是其他优化方面。

代码速度有很多因素,它们通常是由编译器自动处理的低级问题,但如果你知道自己在做什么,这可以使你的代码更快。

首先,显然是Word Size。 64位机器具有更大的字大小(是的,更大通常意味着更好)这样大多数操作可以更快地执行,例如双精度操作(其中double通常意味着2 * 32位)。 64位架构还受益于更大的数据总线,可提供更快的数据传输速率。

其次,管道也很重要。基本指令可以分为不同的状态或阶段,例如,指令通常分为:

  • 获取:从指令缓存中读取指令
  • 解码:对指令进行解码并解释,看看我们要做什么。
  • 执行:执行指令(通常意味着在ALU中执行操作)
  • 内存访问:如果指令必须访问内存(例如从数据缓存加载注册表值),则在此处执行。
  • 回写:将vaues写回目标寄存器。

现在,管道允许处理器在这些阶段上划分指令并同时执行它们,这样当它执行一条指令时,它也会解码下一条指令,然后取出一条指令。

某些说明有依赖关系。如果我一起添加寄存器,执行add指令的阶段将需要在它们实际从内存中恢复之前的值。通过了解管道结构,编译器可以重新排序汇编指令,以便在负载和add之间提供足够的“距离”,以便CPU不必等待。

另一个CPU优化将是超标量,它使用冗余ALU(例如),以便可以同时执行两个添加指令。同样,通过准确了解架构,您可以优化指令的排序以利用。例如,如果编译器检测到代码中不存在依赖关系,则可以重新排列加载和算术运算,以便将算法延迟到所有数据可用的后续位置,然后同时执行4个操作。

这主要是编译器使用的。

在设计应用程序时可以使用什么,哪些可以真正提高代码速度,就是了解缓存策略和组织。最典型的例子是在循环中错误地有序访问双数组:

// Make an array, in memory this is represented as a 1.000.000 contiguous bytes
byte[][] array1 = new byte[1000, 1000];
byte[][] array2 = new byte[1000, 1000;
// Add the array items

for (int j = 0; j < 1000; i++)
  for (int i = 0; i < 1000; j++)
     array1[i,j] = array1[i,j] + array2[i,j]

让我们看看这里发生了什么。

array1 [0,0]进入缓存。由于缓存在块中工作,因此前1000个字节进入缓存,因此缓存将array1 [0,0]保存到array1 [0,999]。

array2 [0,0]可以缓存。再次阻塞,以便你有array2 [0,0]到array2 [0,999]。

在下一步中,我们访问不在缓存中的array1 [1,0],也不是array2 [1,0],因此我们将它们从内存引入缓存。现在,如果我们假设我们的缓存大小非常小,这将使array2 [0 ... 999]从缓存中取出......依此类推。因此,当我们访问array2 [0,1]时,它将不再位于缓存中。缓存对array2或array1没有用。

如果我们重新排序内存访问:

for (int i = 0; i < 1000; i++)
  for (int j = 0; j < 1000; j++)
     array1[i,j] = array1[i,j] + array2[j,i]

无记忆必须从缓存中取出,程序运行速度要快得多。

这些都是天真的,学术性的例子,如果你真的想要或者需要学习计算机体系结构,你需要对体系结构的具体细节有一个非常深入的了解,但这只会在编程编译器时才有用。尽管如此,缓存和基本低级CPU的基本知识可以帮助您提高速度。

例如,这种知识在加密编程中具有极大的价值,你需要处理非常大的数字(如1024位),这样正确的表示可以改善需要执行的下面的数学...... / p>

答案 5 :(得分:3)

在我真正关心的极少数情况下,我遇到的最大的一件事就是地方性。现代CPU运行速度非常快,但它们的缓存容量有限,很容易获得。当他们无法在缓存中找到他们需要的内容时,他们必须从内存中读取,这相对较慢。 (当他们正在寻找的东西不在物理记忆中时,它真的很慢。)

因此,重要的是,尽量保持代码和数据的紧凑。尽可能地卷起循环。

在多线程环境中,存在不同的约束,如果所有线程都在处理来自不同缓存“行”的数据,那么你会更好。

答案 6 :(得分:2)

假设代码具有“速度”是错误的思考问题的方法,IMO。

执行任何给定的操作码需要花费一定的时间。函数的操作数越多,执行所需的时间就越多。知道如何最小化执行的操作码数量(从而创建最快的代码)是我们学习ASM,学习算法等的原因。

答案 7 :(得分:1)

代码的速度基本上受其必须执行的操作量的影响。

它必须做的操作越多,代码就越慢。某些代码执行的操作与其使用的算法直接相关。

所以最后是算法。

此外(仅用于说明代码速度和程序速度之间的差异)

今天的计算机速度已经提高了很多,应用程序的速度并不一定与它使用的代码的速度有关,而是与从一台设备传输到其他设备的数据量有关。

例如(我知道你在这里明确区分)Web应用程序的速度受到通过网络(从数据库到应用服务器以及从应用服务器到客户端)发送的数据量的影响更大而不是花费在其中一个节点内处理数据的时间。

答案 8 :(得分:0)

传统观点认为,更好的算法会为您提供更快的代码,但实际上并非如此简单。 Jalf是对的,它只是取决于。对于编译语言,特定编译器可以比算法更改产生更大的差异。 此外,更快的处理器应该使事情运行得更快。通常。 只需阅读代码完成。它将回答有关优化代码速度的所有问题。

答案 9 :(得分:0)

影响代码速度的因素是什么?

一切。

通常最大的因素是你从未想过的事情。

答案 10 :(得分:0)

在2009硬件上,就像在房地产中一样,在考虑代码速度时,要记住三件事:内存,内存,内存。 (我包括缓存和地点。)

减少分配。减少写入。减少阅读。

在此之后,你经常处于喧嚣之中,并且很难说出明确的结果。 (例如,过去确实在浮点单元中完成的计算几乎总是比在整数单元中完成的类似计算慢。这不再是真的。)

如果你可以同时保持多个整数单元和一个浮点单元,那就更好了。

答案 11 :(得分:0)

是的,分析是一种判断速度的方法。

是的,各种各样的事情都可能导致执行缓慢。

但是,解决性能问题的基本方法是假设你不知道甚至猜不到问题是什么,即使你有一堆加速技巧。

就在今天,我正在开发一个应用程序,打算通过在优化问题中使用闭合形式的梯度计算来加快速度。你猜怎么着?这不是问题所在,它是别的东西。

如果X,Y或Z造成时间浪费,它会在调用堆栈中执行此操作,您可以轻松地看到它。如果它通常不在调用堆栈上,那么它可能不会导致它。 Look here.