什么影响代码的速度?
Posted
技术标签:
【中文标题】什么影响代码的速度?【英文标题】:What influences the speed of code? 【发布时间】:2009-03-06 20:19:26 【问题描述】:查看代码运行速度的方法是性能分析。有针对它的工具等,但我想知道代码速度的因素是什么。
例如,我被告知图像编辑软件将使用按位运算而不是整数变量来计算它们的内容,仅仅是因为它更快。
因此,与二进制相比,使用整数和其他基本类型需要更多的步骤来计算。
肯定还有其他东西,但我没有足够的经验了解操作系统如何连接到您的硬件以及许多编码语言的内部工作原理。
所以我在这里问:你知道是什么影响了代码的速度吗?
不一定程序的速度。
【问题讨论】:
我在这里没有看到实际的问题,部分原因是我不知道代码速度和程序速度之间的差异可能是什么。 我怀疑“代码速度”仅指指令级吞吐量,而程序速度涵盖了所有方面(净带宽、磁盘 I/O、算法选择等)。基本上是微观与宏观优化。 另外,我不知道为什么人们投票结束这个。对我来说,这似乎是一个真正的问题。 OP显然对这个主题了解不多,这是真的,但我上次检查过,这并没有取消人们提问的资格。相反,这可能是填补他知识空白的好方法 @jalf:人们往往会因为他们不喜欢的事情而关闭,但由于他们不发布我们不知道的原因。 我投票决定关闭,因为我无法理解答案是什么。鉴于此,我没有看到真正的问题。如果它被编辑得更清楚,我不想关闭它。 【参考方案1】:整数是二进制。或者更确切地说,整数只是整数,可以用任何数字基数表示。在基数 10 中你会写 13,在基数 16 中你会写 d(或 0xd),在二进制中你会写 1101(或 0b1101)。罗马人会写 XIII。它们都代表数字“十三”的相同概念。在人类中,我们倾向于使用以 10 为底的表示,但是当您要求计算机处理整数时,它使用二进制表示。这并没有什么不同。不管我怎么写,十三加四十五的结果都是一样的。十三 + 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 的性能,通用规则很少。
【讨论】:
【参考方案2】:我认为你错了。整数是二进制数。图像编辑软件将尽其所能避免 浮点 计算,因为与整数或位移运算相比,它们的速度非常慢。
但通常情况下,您首先通过选择正确的算法进行优化,而不是通过诸如担心是后增量还是前增量之类的琐碎小事来优化。
例如:我刚刚花了两天时间来加快我们重新计算一组特定值的速度。我从循环中抽出一些东西并预先计算它,所以它只完成了 M 次而不是 M x N 次,并将一个值存储在一个变量中,而不是每次都从其他地方查找它,因为该值已被使用在 Comparator 所以它会在 Collections.sort 阶段被调用很多。我得到的总执行时间从大约 45 秒到 20 秒。然后我的一位在这里待了很长时间的同事指出,我不需要重新计算这些值,我可以将它们从不同的对象中提取出来。突然它在 2 秒内执行。现在那是我可以相信的优化。
【讨论】:
浮点运算可以和整数一样快,具体取决于上下文。使用 SIMD 指令,它们也可能更快。虽然您更喜欢宏观优化显然是正确的,但这并不能真正回答他的问题,这显然是对事物低级方面的好奇。 很高兴听到经验说话。我唯一会选择的是,您和您的同事都通过思考问题发现了问题。如你所知,我建议让问题本身告诉你它是什么,通过打断它。 迈克,问题是我知道什么在花时间。我只是对问题域了解不够,无法知道我需要的数据已经预先计算并在另一个对象中可用。 我知道你的意思,但这是交易。每隔一段时间,我们的团队就会出现性能恐慌,通常他们“知道”什么会解决它。问题可能需要 50%、20% 和 10% 的时间。他们宁愿知道也不愿诊断,所以知道 20% 的那一个,并且很满意。【参考方案3】:“程序速度”通常归结为算法选择。错误的算法可以将 2 秒的任务变成 2 分钟或更糟的任务。当您专注于做到这一点时,您将看到最佳的性能提升。
一旦你有了一个好的算法,你可能仍然需要一个更高效的实现。实现这一点通常依赖于“代码速度”类型的选择。有几件事情需要考虑,它们往往非常依赖于硬件。一个处理器的优化实际上可能会减慢其他处理器的代码。
一些“代码速度”因素:
整数与浮点数 分支、函数调用、上下文切换的成本 CPU 指令流水线中断 保持内存访问的局部性,缓存一致性。【讨论】:
【参考方案4】:进一步扩展 Oscar Reyes 所说的内容,即使在最低级别,数据传输实际上也是性能的关键因素,因此 CPU 执行的操作的数量和类型对整体性能都至关重要。
对于像 x86 这样的 CISC 处理器尤其如此,其中指令本身可能具有不同的循环计数,尽管现代设计大多缓解了这一点。但是,在内存加载和存储操作的情况下,所有 CPU 都是如此,这可能比任何其他操作都贵很多、很多、很多倍。考虑到现代 CPU 的时钟频率与内存延迟的关系,如果出现页面错误并且必须写入磁盘,您可能会看到数十条指令在缓存未命中的情况下被浪费到数百万条。在许多情况下,当考虑到缓存行为以及加载和存储行为时,执行大量算术运算来重新计算一个值实际上可能要快得多,而不是缓存它并从内存中重新读取它,即使负载是一条汇编指令与执行计算的许多指令。在某些情况下,考虑性能仅受加载和存储限制并将其他操作视为“免费”实际上可能是有意义的,尽管这自然取决于具体情况。
【讨论】:
【参考方案5】:代码速度主要受计算机架构的低级优化影响,包括 CPU 和其他优化。
代码速度有很多因素,它们通常是由编译器自动处理的低级问题,但如果您知道自己在做什么,这可以使您的代码更快。
首先,显然是字长。 64 位机器具有更大的字长(是的,更大通常意味着更好),以便可以更快地执行大多数操作,例如双精度操作(其中双精度通常表示 2 * 32 位)。 64 位架构还受益于提供更快数据传输速率的更大数据总线。
其次,管道也很重要。基本指令可以分为不同的状态或阶段,例如,指令通常分为:
Fetch:从指令缓存中读取指令 解码:对指令进行解码并进行解释,看看我们必须做什么。 Execute:指令被执行(通常意味着在 ALU 中进行操作) 内存访问:如果指令必须访问内存(例如从数据缓存加载注册表值),则在此处执行。 写回:将值写回目标寄存器。现在,流水线允许处理器在这些阶段划分指令并同时执行它们,以便在执行一条指令的同时解码下一条指令并获取之后的指令。
有些指令有依赖关系。如果我一起添加到寄存器中,则 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 位),以便正确的表示可以改进需要执行的底层数学......
【讨论】:
【参考方案6】:在我真正关心的极少数情况下,我遇到的最大的一件事就是地方性。现代 CPU 运行速度非常快,但它们的高速缓存数量有限,而且很容易获得。当他们在缓存中找不到他们需要的东西时,他们必须从内存中读取,这是比较慢的。 (当他们寻找的东西不在物理内存中时,它真的很慢。)
因此,当重要时,请尽量保持代码和数据紧凑。尽可能多地卷起循环。
在多线程环境中,存在不同的约束,如果您的所有线程都在处理来自不同缓存“行”的数据,您会做得更好。
【讨论】:
【参考方案7】:假设代码具有“速度”是思考问题的错误方式,IMO。
执行任何给定的操作码都需要固定的时间。函数的操作码越多,执行所需的时间就越多。知道如何最小化执行的操作码数量(从而创建最快的代码)是我们学习 ASM、研究算法等的原因。
【讨论】:
虽然这通常是正确的,但值得指出的是,在最低级别,某些操作码比其他操作码花费的时间要长得多。每种类型的 CPU 都有一组不同的“昂贵”的。 有些需要比其他更长的时间。甚至一个特定的操作码也可能需要可变的时间,具体取决于参数以及之前的指令。并且可以并行执行多条指令,并且 CPU 会愉快地对它们进行动态重新排序,因此分配固定成本是不可能的 @AShelly & @jalf:只需将“opcode”一词替换为“cycle”即可。为了最大限度地减少周期,只需确保每个周期都是真正必要的。我们将自己与“速度”和“瓶颈”之类的词混为一谈。真正的问题是做不必要的事情。【参考方案8】:代码的速度会受到影响,主要是受它必须执行的操作量的影响。
它必须做的操作越多,代码就会越慢。某些代码执行的操作与其使用的算法直接相关。
所以最后是算法。
另外(仅说明代码速度和程序速度的区别)
当今计算机的速度已经大大提高,以至于应用程序的速度与它使用的代码的速度无关,而与它从一个设备传输到其他设备的数据量有关。
例如(我知道您在这里标记了一个明显的区别)Web 应用程序的速度受通过网络发送的数据量(从数据库到应用程序服务器以及从应用程序服务器到客户端)的影响更大比它在其中一个节点内处理该数据所花费的时间。
【讨论】:
你说得对,奥斯卡。我只对“算法”这个词有疑问,对我们大多数人来说,这意味着哈希编码与冒泡排序之类的东西。根据我的经验,这通常不是问题。问题在于数据结构的泛滥和过度设计。【参考方案9】:传统观点认为,更好的算法会为您提供更快的代码,但实际上并非如此简单。 Jalf 是对的,这取决于。对于已编译的语言,特定的编译器可以产生比算法更改更大的差异。 此外,更快的处理器应该让事情运行得更快。通常。 只需阅读代码完成。它将回答您关于优化代码以提高速度的所有问题。
【讨论】:
【参考方案10】:影响代码速度的因素是什么?
一切。
通常最大的因素是你从未想过的事情。
【讨论】:
【参考方案11】:在 2009 年的硬件上,就像在房地产中一样,在考虑代码速度时只需要记住三件事:内存、内存、内存。 (我包括缓存和局部性。)
减少分配。减少写入。减少读取。
在那之后,您通常会陷入困境,并且很难说出分类结果。 (例如,在浮点单元中完成的计算几乎总是比在整数单元中完成的类似计算慢,这是事实。现在不是这样了。)
如果您可以同时保持多个整数单元和一个浮点单元忙碌,那就太好了。
【讨论】:
【参考方案12】:是的,分析是一种判断速度的方法。
是的,各种各样的事情都会导致执行缓慢。
但是,处理性能问题的基本方法是假设您不知道甚至可以猜测问题是什么,即使您有很多加速技巧。 p>
就在今天,我正在开发一个应用程序,打算通过在优化问题中使用封闭形式的梯度计算来使其更快。你猜怎么了?这不是问题所在,而是其他问题。
如果 X、Y 或 Z 导致时间浪费,则在执行此操作时它将位于调用堆栈上,您可以轻松看到它。如果它通常不在调用堆栈上,那么它可能不是导致它的原因。 Look here.
【讨论】:
以上是关于什么影响代码的速度?的主要内容,如果未能解决你的问题,请参考以下文章