jer*_*jvl 600 language-agnostic optimization performance
这个网站上已经存在很多性能问题,但是我发现几乎所有这些都是特定于问题且相当狭窄的问题.几乎所有人都重复这些建议,以避免过早优化.
我们假设:
我在这里寻找的是在一个关键算法中挤出最后几个百分点的策略和技巧,除此之外别无他法.
理想情况下,尝试使答案语言不可知,并在适用的情况下指出建议策略的任何缺点.
我将使用我自己的初步建议添加回复,并期待Stack Overflow社区可以想到的任何其他内容.
Mik*_*vey 422
好的,你将问题定义为似乎没有太大改进的地方.根据我的经验,这是相当罕见的.我试着在十一月'93解释这个布斯博士的文章中,通过从传统设计良好的非平凡的程序,没有明显的浪费,并开始服用它通过一系列的优化,直到它的挂钟时间从48秒减少秒到1.1秒,源代码大小减少了4倍.我的诊断工具就是这个.变化的顺序是这样的:
发现的第一个问题是列表集群(现在称为"迭代器"和"容器类")的使用占了一半以上的时间.用相当简单的代码替换它们,将时间缩短到20秒.
现在最大的时间接受者更多的是建立名单.作为一个百分比,它之前没那么大,但现在是因为更大的问题被删除了.我找到了加速它的方法,时间下降到17秒.
现在很难找到明显的罪魁祸首,但是我可以做一些较小的琐事,时间下降到13秒.
现在我好像已经撞墙了.样品正在告诉我它到底在做什么,但我似乎找不到任何可以改进的东西.然后,我在其事务驱动结构上反思程序的基本设计,并询问它所执行的所有列表搜索是否实际上都是由问题的要求强制执行的.
然后,我偶然发现从较小的集源的重新设计,其中程序代码实际上是(通过预处理宏)生成,其中程序没有不断搞清楚事情的程序员知道有相当的可预见性.换句话说,不要"解释"要做的事情的顺序,"编译"它.
现在,因为它变得如此之快,所以很难进行抽样,所以我给它做了10倍的工作量,但以下时间是基于原始工作量.
更多的诊断表明,它正在花时间进行队列管理.内嵌这些将时间缩短到7秒.
现在一个重要的时间是我一直在做的诊断印刷.冲洗 - 4秒.
现在最大的时间是调用malloc和free.回收对象 - 2.6秒.
继续抽样,我仍然发现不是绝对必要的操作--1.1秒.
总加速系数:43.6
现在没有两个程序是相似的,但在非玩具软件中,我总是看到这样的进展.首先,你得到了简单的东西,然后就越难,直到你达到收益递减的程度.然后你获得的洞察力很可能导致重新设计,开始新一轮的加速,直到你再次达到收益递减.现在,这是它可能是有意义的怀疑是否点++i或i++或for(;;)或while(1)更快:题型我看到那么经常SO.
PS可能想知道为什么我没有使用分析器.答案是,这些"问题"中几乎每一个都是一个函数调用站点,它会对样本进行精确定位.即使在今天,Profilers也几乎没有意识到语句和调用指令比整个函数更重要,更容易定位,更容易修复.我实际上构建了一个分析器来实现这一点,但是为了与代码正在做的真正的肮脏的亲密关系,没有任何东西可以替代你的手指.样本数量很小不是问题,因为找到的问题都不是很小,很容易错过.
补充:jerryjvl请求了一些例子.这是第一个问题.它由少量独立的代码行组成,占用了一半以上的时间:
/* IF ALL TASKS DONE, SEND ITC_ACKOP, AND DELETE OP */
if (ptop->current_task >= ILST_LENGTH(ptop->tasklist){
. . .
/* FOR EACH OPERATION REQUEST */
for ( ptop = ILST_FIRST(oplist); ptop != NULL; ptop = ILST_NEXT(oplist, ptop)){
. . .
/* GET CURRENT TASK */
ptask = ILST_NTH(ptop->tasklist, ptop->current_task)
Run Code Online (Sandbox Code Playgroud)
这些是使用列表集群ILST(类似于列表类).它们以通常的方式实现,"信息隐藏"意味着类的用户不应该关心它们是如何实现的.当写出这些行(大约800行代码中)时,没有想到这些行可能是"瓶颈"(我讨厌这个词).它们只是推荐的做事方式.事后很容易说这些应该是可以避免的,但根据我的经验,所有性能问题都是这样的.通常,尽量避免产生性能问题是很好的.找到并修复创建的内容会更好,即使它们"应该已经避免"(事后看来).我希望这会带来一些味道.
这是第二个问题,分为两个部分:
/* ADD TASK TO TASK LIST */
ILST_APPEND(ptop->tasklist, ptask)
. . .
/* ADD TRANSACTION TO TRANSACTION QUEUE */
ILST_APPEND(trnque, ptrn)
Run Code Online (Sandbox Code Playgroud)
这些是通过将项目附加到其末尾来构建列表.(修复是收集数组中的项目,并一次构建列表.)有趣的是,这些语句只花费(即在调用堆栈上)原始时间的3/48,因此它们不在事实上一开始就是一个大问题.然而,在消除了第一个问题之后,他们花费了3/20的时间,所以现在是"更大的鱼".一般来说,就是这样.
我可以补充一点,这个项目是从我帮助过的真实项目中提炼出来的.在该项目中,性能问题更加引人注目(与加速一样),例如在内部循环中调用数据库访问例程以查看任务是否已完成.
参考添加:原始和重新设计的源代码可以在www.ddj.com中找到,1993年,在文件9311.zip中,文件slug.asc和slug.zip.
编辑2011/11/26:现在有一个sourceforge项目,包含Visual C++中的源代码,以及如何调整它的详细描述.它只经历了上述场景的前半部分,并且它不遵循完全相同的序列,但仍然获得2-3个数量级的加速.
jer*_*jvl 188
建议:
ken*_*418 164
当你无法再提高性能时 - 看看你是否可以提高感知性能.
您可能无法更快地使用fooCalc算法,但通常有一些方法可以使您的应用程序对用户更具响应性.
几个例子:
这些不会使您的程序更快,但它可能会让您的用户更快乐.
Cra*_*rks 138
我大部分时间都在这个地方度过.广泛的描述是运行你的探查器并让它记录:
__restrict自由地向编译器承诺别名.还有一件事我喜欢做:
sis*_*sve 78
投入更多硬件!
jer*_*jvl 58
更多建议:
避免I/O:任何I/O(磁盘,网络,端口等)总是比执行计算的任何代码慢得多,因此请删除任何您不需要的I/O.
预先移动I/O:预先加载计算所需的所有数据,以便在关键算法的核心内没有重复的I/O等待(可能因此重复磁盘搜索,当一次命中加载所有数据时可能会避免寻找).
延迟I/O:在计算结束之前不要写出结果,将它们存储在数据结构中,然后在完成工作时将其一次性转储.
螺纹I/O:对于那些大胆的人,将'I/O预先'或'延迟I/O'与实际计算相结合,将加载移动到并行线程中,这样当您加载更多数据时,您可以工作计算您已有的数据,或者在计算下一批数据时,您可以同时写出最后一批数据.
HLG*_*GEM 48
由于许多性能问题都涉及数据库问题,因此在调优查询和存储过程时,我将为您提供一些具体的内容.
在大多数数据库中避免使用游标.避免循环.大多数情况下,数据访问应该基于集合,而不是记录处理.这包括当您要一次插入1,000,000条记录时不重用单个记录存储过程.
切勿使用select*,只返回您实际需要的字段.如果存在任何连接,则尤其如此,因为连接字段将被重复,从而导致服务器和网络上的不必要的负载.
避免使用相关子查询.使用连接(包括可能的连接到派生表)(我知道这适用于Microsoft SQL Server,但在使用不同的后端时测试建议).
索引,索引,索引.如果适用于您的数据库,请更新这些统计信息.
使查询可以进行搜索.意义避免使得无法使用索引的事情,例如在like子句的第一个字符或连接中的函数中使用通配符或作为where语句的左侧部分.
使用正确的数据类型.在日期字段上进行日期数学比在必须尝试将字符串数据类型转换为日期数据类型更快,然后进行计算.
切勿将任何类型的循环放入触发器中!
大多数数据库都有办法检查查询执行的执行方式.在Microsoft SQL Server中,这称为执行计划.首先检查那些问题区域.
在确定需要优化的内容时,请考虑查询运行的频率以及运行所需的时间.有时候,从轻微的调整到每天运行数百万次的查询可以获得更多的性能,而不是每月只运行一次的long_running查询擦除时间.
使用某种分析器工具来查找实际发送到数据库和从数据库发送的内容.我记得有一次我们无法弄清楚为什么页面在存储过程很快时加载速度太慢,并通过剖析网页要求查询多次而不是一次查询.
探查器还可以帮助您找到阻止谁的人.由于来自其他查询的锁定,一些在单独运行时快速执行的查询可能会变得非常慢.
小智 29
今天唯一最重要的限制因素是有限的内存bandwitdh.多核只会使这种情况变得更糟,因为带宽在核心之间共享.此外,用于实现高速缓存的有限芯片面积也在核心和线程之间划分,甚至更加恶化了这个问题.最后,保持不同高速缓存一致所需的芯片间信令也随着核心数量的增加而增加.这也增加了罚款.
这些是您需要管理的效果.有时通过微观管理代码,但有时通过仔细考虑和重构.
很多评论已经提到缓存友好代码.至少有两种不同的风格:
第一个问题特别涉及使数据访问模式更加规则,允许硬件预取器高效工作.避免动态内存分配,将内存中的数据对象分散开来.使用线性容器而不是链接列表,哈希和树.
第二个问题与改进数据重用有关.更改算法以处理适合可用缓存的数据子集,并在数据仍处于缓存中时尽可能多地重用这些数据.
更紧密地打包数据并确保使用热循环中缓存行中的所有数据,这将有助于避免这些其他影响,并允许在缓存中安装更多有用的数据.
Joh*_*ski 25
aso*_*ove 16
虽然我喜欢Mike Dunlavey的回答,但实际上它是一个很好的答案,确实有支持的例子,我认为它可以非常简单地表达出来:
找出最先花费最多时间的东西,并了解原因.
它是时间点的识别过程,可帮助您了解必须优化算法的位置.这是唯一一个无所不包的语言不可知的答案,我可以找到一个已经应该完全优化的问题.还假设您希望在追求速度时独立于架构.
因此,尽管可以优化算法,但是它的实现可能不是.标识允许您知道哪个部分是哪个:算法或实现.所以,无论哪个时间最重要的是你的主要候选人.但是既然你说你想要挤出最后几个百分比,你可能还想检查较小的部分,那些你最初没有仔细检查过的部分.
最后,通过不同方式实现相同解决方案或可能不同的算法的性能数据的一些试验和错误可以带来有助于识别时间浪费和节省时间的见解.
HPH,asoudmove.
pli*_*nth 15
Dro*_*per 12
MPe*_*ier 12
分而治之
如果正在处理的数据集太大,则循环遍历它的块.如果您已经完成了正确的代码,那么实现应该很容易.如果你有一个单片程序,现在你知道的更好.
gna*_*nat 11
首先,正如前几个答案中所提到的,了解一下你的表现是什么 - 它是内存或处理器,网络或数据库还是别的东西.取决于......
......如果它的记忆 - 找到很久以前由Knuth写的一本书,"计算机编程艺术"系列之一.很可能是关于排序和搜索的问题 - 如果我的记忆错了,那么你必须找出他谈论如何处理慢速磁带数据存储的问题.精神上将他的内存/磁带对分别转换为你的一对缓存/主内存(或一对L1/L2缓存).研究他描述的所有技巧 - 如果你找不到解决问题的方法,那就聘请专业的计算机科学家进行专业研究.如果您的内存问题偶然发生在FFT(执行基数为2的蝴蝶时缓存未达到位反转索引),那么就不要雇用科学家 - 而是手动优化传递,直到您获胜或获得走到尽头.你提到挤出最后几个百分点吧?如果确实很少,你很可能会获胜.
...如果是处理器 - 切换到汇编语言.研究处理器规范 - 什么需要滴答,VLIW,SIMD.函数调用很可能是可替换的滴答滴答者.学习循环转换 - 管道,展开.乘法和除法可以用位移替换/插值(乘以小整数可以用加法替换).尝试使用更短的数据 - 如果你幸运的话,一个64位的指令可能会被替换为32位上的两位或16位上的4位或8位上的8位数.尝试更长的数据 - 例如,您的浮点计算可能比特定处理器的双重计算慢.如果您有三角函数,请使用预先计算的表格进行对抗; 还要记住,如果精度损失在允许的限度内,那么小值的正弦值可能会被该值替换.
...如果它是网络 - 想想压缩你传递的数据.用二进制替换XML传输.研究方案.如果可以以某种方式处理数据丢失,请尝试使用UDP而不是TCP.
...如果是数据库,那么,去任何数据库论坛并寻求建议.内存数据网格,优化查询计划等等.
HTH :)
缓存!一种廉价的方法(在程序员的努力下)几乎可以做任何事情就是在程序的任何数据移动区域添加一个缓存抽象层.无论是I/O还是传递/创建对象或结构.通常,很容易将缓存添加到工厂类和读写器中.
有时候缓存不会给你带来太多帮助,但是这是一种简单的方法,只需添加缓存,然后在没有帮助的地方禁用缓存.我经常发现这可以获得巨大的性能而无需对代码进行微观分析.
我认为这已经以不同的方式说过了.但是当你处理一个处理器密集型算法时,你应该以最重要的内容为代价来简化内部循环中的所有内容.
对某些人来说这似乎是显而易见的,但无论我使用哪种语言,我都会尝试关注这一点.例如,如果您正在处理嵌套循环,并且您发现有机会将某些代码放在某个级别,那么在某些情况下您可以大大加快代码速度.作为另一个例子,有些事情需要考虑,比如使用整数而不是浮点变量,并且尽可能使用乘法而不是除法.同样,这些是你最内循环应该考虑的事情.
有时您可能会发现在内循环内对整数执行数学运算的好处,然后将其缩小为可以在之后使用的浮点变量.这是在一个部分牺牲速度以提高另一部分的速度的一个例子,但在某些情况下,回报可能是值得的.
小智 8
我花了一些时间来优化在低带宽和长延迟网络(例如卫星,远程,离岸)上运行的客户端/服务器业务系统,并且能够通过相当可重复的过程实现一些显着的性能改进.
措施:首先了解网络的基础容量和拓扑.与业务中的相关网络人员交谈,并利用ping和traceroute等基本工具在典型的运营期间(至少)建立每个客户端位置的网络延迟.接下来,对显示有问题症状的特定最终用户功能进行准确的时间测量.记录所有这些测量值,以及它们的位置,日期和时间.考虑在客户端应用程序中构建最终用户"网络性能测试"功能,允许高级用户参与改进过程; 当你处理因表现不佳的系统而感到沮丧的用户时,像这样赋予他们权力可能会产生巨大的心理影响.
分析:使用可用的任何和所有日志记录方法准确确定在执行受影响的操作期间正在传输和接收的数据.理想情况下,您的应用程序可以捕获客户端和服务器发送和接收的数据.如果这些也包括时间戳,甚至更好.如果没有足够的日志记录(例如,封闭系统或无法将修改部署到生产环境中),请使用网络嗅探器并确保您真正了解网络级别正在进行的操作.
缓存:查找重复传输静态或不经常更改的数据并考虑适当的缓存策略的情况.典型示例包括"选择列表"值或其他"参考实体",其在一些商业应用中可能令人惊讶地大.在许多情况下,用户可以接受他们必须重新启动或刷新应用程序以更新不经常更新的数据,特别是如果它可以显着地显示常用的用户界面元素.确保您了解已部署的缓存元素的实际行为 - 许多常见的缓存方法(例如HTTP ETag)仍然需要网络往返以确保一致性,并且在网络延迟昂贵的情况下,您可以完全避免它一种不同的缓存方法.
并行化:查找逻辑上不需要严格按顺序发出的顺序事务,并重新编写系统以并行发布它们.我处理了一个案例,其中端到端请求具有~2s的固有网络延迟,这对于单个事务不是问题,但是在用户重新获得对客户端应用程序的控制之前需要6次连续2次往返,它成了沮丧的巨大根源.发现这些事务实际上是独立的,允许它们并行执行,从而将最终用户延迟减少到非常接近单次往返的成本.
组合:必须按顺序执行顺序请求,寻找将它们组合成单个更全面的请求的机会.典型示例包括创建新实体,然后是将这些实体与其他现有实体相关联的请求.
压缩:寻找机会利用有效负载的压缩,或者通过用二进制替换文本形式,或者使用实际的压缩技术.许多现代(即十年内)技术堆栈几乎透明地支持这一点,因此请确保它已配置好.我经常对压缩的重大影响感到惊讶,因为似乎很明显问题基本上是延迟而不是带宽,发现它允许事务适合单个数据包或以其他方式避免数据包丢失,因此具有特大优势对绩效的影响.
重复:回到开头并重新测量您的操作(在相同的位置和时间)并进行改进,记录并报告您的结果.与所有优化一样,一些问题可能已经解决,暴露出现在占主导地位的其他问题.
在上面的步骤中,我专注于与应用程序相关的优化过程,但当然您必须确保以最有效的方式配置底层网络本身以支持您的应用程序.让网络专家参与业务,并确定他们是否能够应用容量改进,QoS,网络压缩或其他技术来解决问题.通常情况下,他们无法理解您的应用程序的需求,因此您必须配备(在分析步骤之后)与他们讨论,并为您将要求他们承担的任何成本制定业务案例. .我遇到过错误的网络配置导致应用程序数据通过慢速卫星链路而不是陆上链路传输的情况,原因很简单,因为它使用的网络专家并没有"众所周知".显然纠正这样的问题会对性能产生巨大影响,根本不需要软件代码或配置更改.
Did you know that a CAT6 cable is capable of 10x better shielding off extrenal inteferences than a default Cat5e UTP cable?
对于任何非离线项目,虽然拥有最好的软件和最好的硬件,但如果你的吞吐量很弱,那么这条细线就会挤压数据并给你延迟,虽然只有几毫秒......但如果你在谈论最后一滴,对于任何发送或接收的包装,这是一些24/7的下降.
不像以前的答案那样深度或复杂,但这里有:(这些是更初级/中级)
不可能说.这取决于代码的样子.如果我们可以假设代码已经存在,那么我们可以简单地看一下并从中找出,如何优化它.
更好的缓存局部性,循环展开,尝试消除长依赖链,以获得更好的指令级并行性.在可能的情况下,首选条件移动分支.尽可能利用SIMD指令.
了解您的代码正在做什么,并了解它正在运行的硬件.然后,确定您需要做什么来提高代码性能变得相当简单.这真的是我能想到的唯一真正的一般建议.
嗯,那,和"在SO上显示代码并询问针对该特定代码的优化建议".
小智 5
如果更好的硬件是一个选项,那么一定要去做.除此以外
以下是我使用的一些快速而肮脏的优化技术.我认为这是"第一次通过"优化.
了解花费时间的地方确切了解花时间.它是文件IO吗?是CPU时间吗?是网络吗?是数据库吗?如果不是瓶颈,那么优化IO是没用的.
了解您的环境 了解优化位置通常取决于开发环境.例如,在VB6中,通过引用传递比传递值慢,但在C和C++中,通过引用传递速度要快得多.在C中,如果返回代码指示失败,尝试某些操作并执行不同的操作是合理的,而在Dot Net中,捕获异常比在尝试之前检查有效条件要慢得多.
指数构建上频繁查询数据库字段的索引.你几乎总能以空间换取速度.
避免查找 在要优化的循环内部,我避免必须进行任何查找.找到循环外的偏移量和/或索引,并重用其中的数据.
最小化IO尝试以减少必须通过网络连接读取或写入的次数的方式进行设计
减少抽象代码必须处理的抽象层越多,它就越慢.在关键循环内部,减少抽象(例如,揭示避免额外代码的低级方法)
具有用户界面的项目的Spawn Threads,产生新线程以执行较慢的任务使应用程序感觉更具响应性,尽管不是.
预处理您通常可以交换空间以提高速度.如果有计算或其他强烈操作,请查看您是否可以在进入关键循环之前预先计算某些信息.
如果您有很多高度并行的浮点数学,尤其是单精度数学,请尝试使用 OpenCL 或(对于 NVidia 芯片)CUDA 将其卸载到图形处理器(如果存在)。GPU 在其着色器中具有巨大的浮点计算能力,这比 CPU 强得多。
添加这个答案,因为我没有看到它包含在所有其他人.
这至少适用于C/C++,即使您已经认为自己没有转换 - 有时可以测试在需要性能的函数周围添加编译器警告,尤其是在循环内进行转换时要小心.
GCC spesific:您可以通过在代码周围添加一些详细的编译指示来测试它,
#ifdef __GNUC__
# pragma GCC diagnostic push
# pragma GCC diagnostic error "-Wsign-conversion"
# pragma GCC diagnostic error "-Wdouble-promotion"
# pragma GCC diagnostic error "-Wsign-compare"
# pragma GCC diagnostic error "-Wconversion"
#endif
/* your code */
#ifdef __GNUC__
# pragma GCC diagnostic pop
#endif
Run Code Online (Sandbox Code Playgroud)
我已经看到过这样的情况,你可以通过减少这样的警告引起的转换来获得几个百分点的加速.
在某些情况下,我有一个带有严格警告的标题,我会将其包括在内以防止意外转换,但这是一种权衡,因为您最终可能会添加大量演员表来安静地进行故意转换,这可能会使代码更加混乱收益.
| 归档时间: |
|
| 查看次数: |
75955 次 |
| 最近记录: |