(这主要是针对那些对低延迟系统有特定知识的人,以避免人们只是用没有根据的意见来回答)。
您是否觉得在编写“好的”面向对象的代码与编写非常快速的低延迟代码之间需要权衡?例如,避免在C ++中使用虚函数/多态性的开销等-重写看上去很讨厌但很快的代码等?
这是有道理的-谁在乎它是否看起来丑陋(只要它可以维护)-如果您需要速度,就需要速度吗?
我很想听听在这些领域工作过的人的来信。
(这主要是针对那些对低延迟系统有特定知识的人,以避免人们只是用没有根据的意见来回答)。
您是否觉得在编写“好的”面向对象的代码与编写非常快速的低延迟代码之间需要权衡?例如,避免在C ++中使用虚函数/多态性的开销等-重写看上去很讨厌但很快的代码等?
这是有道理的-谁在乎它是否看起来丑陋(只要它可以维护)-如果您需要速度,就需要速度吗?
我很想听听在这些领域工作过的人的来信。
Answers:
您是否觉得在编写“好的”面向对象的代码与编写非常[低]的低延迟代码之间需要权衡?
是。
这就是为什么存在短语“过早优化”的原因。它的存在是迫使开发人员衡量其性能,并且仅优化将对性能产生影响的代码,同时从一开始就明智地设计其应用程序体系结构,以使其不会在繁重的工作中掉下去。
这样,您可以最大程度地保留漂亮的,结构良好的,面向对象的代码,并且仅使用难看的代码来优化那些重要的小部分。
是的,我给出的示例不是C ++ vs. Java,而是Assembly vs. COBOL,因为这就是我所知道的。
两种语言都非常快,但是即使是COBOL,在编译时也将更多的指令放到指令集中,而不是在汇编中自己编写这些指令。
与在C ++中使用继承/多态性相比,可以将相同的想法直接应用于编写“难看的代码”的问题。我认为有必要编写难看的代码,如果最终用户需要不到一秒的交易时间范围,那么无论如何发生,这都是程序员的工作。
话虽这么说,无论代码多么丑陋,自由地使用注释都可以大大提高程序员的功能和可维护性。
是的,存在折衷。通过这种方式,我的意思是更快更好的代码并不一定更好-权衡“快速代码”带来的量化收益与实现该速度所需的代码更改的维护复杂性之间的权衡。
权衡来自业务成本。更复杂的代码需要更熟练的程序员(以及具有更专注技能的程序员,例如具有CPU体系结构和设计知识的程序员),花费更多时间阅读和理解代码以及修复错误。与正常编写的代码相比,开发和维护此类代码的商业成本可能在10倍至100倍的范围内。
在某些行业中,这种维护成本是合理的,在该行业中,客户愿意为非常快速的软件支付很高的溢价。
某些速度优化比其他优化具有更好的投资回报率(ROI)。即,与通常编写的代码相比,可以应用某些优化技术对代码的可维护性(保留更高级别的结构和更低级别的可读性)的影响较小。
因此,企业主应:
这些折衷是针对具体情况的。
没有经理和产品所有者的参与,就无法最佳地决定这些。
这些是特定于平台的。例如,台式机和移动CPU有不同的注意事项。服务器和客户端应用程序也有不同的注意事项。
是的,通常,更快的代码看起来与正常编写的代码不同。任何不同的代码都将花费更多时间阅读。这是否意味着丑陋在旁观者的眼中。
我接触过的技术有:(不要求任何专业知识的水平)短向量优化(SIMD),细粒度的任务并行性,内存预分配和对象重用。
尽管SIMD通常不需要更高级别的结构更改,但SIMD通常会对低级别的可读性产生严重影响(前提是该API在设计时就考虑了防止瓶颈的问题)。
某些算法可以轻松转换为SIMD(可向量化)。一些算法需要更多的计算重排才能使用SIMD。在极端情况下,例如波前SIMD并行性,必须编写全新的算法(以及获得专利的实现)才能利用。
细粒度的任务并行化需要将算法重新排列到数据流图中,并反复对算法应用功能(计算)分解,直到无法获得更多的利润。分解的阶段通常与延续样式链接在一起,延续样式是从函数编程中借用的概念。
通过功能(计算)分解,原本可以按线性且概念清晰的顺序正常编写的算法(可以按编写顺序执行的代码行)必须分解为多个片段,然后分配给多个功能或课程。(请参见下面的算法目标化。)此更改将极大地阻碍不熟悉产生这种代码的分解设计过程的其他程序员。
为了使此类代码可维护,此类代码的作者必须编写详尽的算法文档-远远超出了通常为普通代码编写的代码注释或UML图类型。这类似于研究人员撰写学术论文的方式。
不,快速代码不必与面向对象相矛盾。
换句话说,可以实现仍然是面向对象的非常快速的软件。但是,在该实现的低端(在发生大部分计算的基本操作级别上),对象设计可能与从面向对象设计(OOD)获得的设计有很大的出入。下层设计面向算法目标化。
面向对象编程(OOP)的一些好处,例如封装,多态性和组合,仍然可以从低级算法目标化中获得。这是在此级别上使用OOP的主要理由。
失去了面向对象设计(OOD)的大多数好处。最重要的是,底层设计没有直观性。 在没有首先完全了解算法是如何进行变换和分解的情况下,其他程序员无法学习如何使用低级代码,并且无法从生成的代码中获得这种理解。
我从中看到的一些研究摘录表明,干净易读的代码通常比更复杂的难读代码更快。部分原因在于优化程序的设计方式。与对中间结果进行计算相比,它们在将变量优化到寄存器中时往往要好得多。与使用较长的复杂方程式相比,使用单个运算符生成最终结果的较长的分配序列可能会得到更好的优化。较新的优化器可能已减少了干净代码与复杂代码之间的区别,但我怀疑他们已消除了它。
如果需要,可以以干净的方式添加其他优化,例如循环展开。
为提高性能而进行的任何优化都应附有适当的注释。这应该包括一条声明,指出它是作为优化添加的,最好使用之前和之后的性能度量。
我发现80/20规则适用于我优化的代码。根据经验,我不会优化任何不会花费至少80%的时间的事情。然后,我的目标是(通常是实现)性能提高10倍。这样可以将性能提高大约4倍。我实现的大多数优化都没有使代码的“美观”程度大大降低。你的旅费可能会改变。
如果说丑陋,意味着您难以理解/理解其他开发人员将重用它或需要理解它的水平,那么我想说,优雅,易于阅读的代码几乎总会最终使您获得从长远来看,您必须维护的应用程序可以提高性能。
否则,有时会有足够的性能获胜,因此值得将丑陋的东西放在一个带有杀手级界面的漂亮盒子中,但是根据我的经验,这是一个非常罕见的难题。
考虑一下基本的回避工作。保留一些奥秘的技巧,以防实际出现性能问题。而且,如果您确实必须编写某些人只能通过熟悉特定的优化才能理解的内容,那么您应该做的是至少可以从重用代码的角度使丑陋易于理解。表现极差的代码很少这样做,因为开发人员过分思考下一个家伙将要继承的内容,但是如果频繁更改是应用程序的唯一常数(以我的经验,大多数Web应用程序都是如此),那么僵化/僵化的代码就是实际上,很难修改的原因是乞求恐慌的混乱开始在您的代码库中弹出。从长远来看,清洁和精益对性能更有利。
您是否觉得在编写“好的”面向对象的代码与编写非常快速的低延迟代码之间需要权衡?例如,避免在C ++中使用虚函数/多态性的开销等-重写看上去很讨厌但很快的代码等?
我从事的领域是吞吐量,而不是延迟,但它对性能至关重要,我会说“ sorta”。
但是问题是,太多的人完全错误地理解了他们的绩效观念。新手经常会犯错,他们的整个“计算成本”概念模型都需要重新设计,只有算法复杂性才是他们唯一可以正确解决的问题。中间人弄错了很多事情。专家认为有些错误。
使用可以提供诸如高速缓存未命中和分支错误预测之类的指标的准确工具进行测量,可以使该领域中所有专业知识水平的所有人都受到检查。
衡量也是指出无法优化的内容。专家花费的时间通常比新手少,因为他们正在优化真实的热点,而不是基于对可能慢的预感(在极端情况下,可能会诱使人们进行微优化),在黑暗中优化野刺。关于代码库中的所有其他行)。
除此之外,性能设计的关键来自接口部分的设计部分。缺乏经验的问题之一是,绝对实现指标往往会在早期发生转变,例如在某些通用上下文中调用间接函数的成本,就好像成本(从优化程序的角度立即理解)视图而不是分支视图)是避免在整个代码库中使用它的原因。
成本是相对的。虽然间接函数调用会产生成本,但是,所有成本都是相对的。如果您花一倍的时间来调用一个循环遍历数百万个元素的函数,则担心此成本就像花费数小时来讨价还价以购买十亿美元的产品,只是得出结论不购买该产品,因为它一分钱太贵了。
性能的接口设计方面通常会更早地寻求将这些成本推到更粗略的水平。例如,与其为单个粒子支付运行时抽象成本,不如将其推高至粒子系统/发射器的水平,从而有效地将粒子渲染为实现细节和/或仅将此粒子集合的原始数据渲染。
因此,面向对象的设计不一定要与性能设计(无论是等待时间还是吞吐量)不兼容,但是用一种专注于它的语言进行的诱惑就可以建模越来越小的颗粒对象,而最新的优化器无法救命。它无法像通过为软件的内存访问模式产生有效的SoA表示的方式合并代表单个点的类那样的事情。接口设计在粗糙级别上建模的点集合提供了这种机会,并允许根据需要迭代到越来越多的最佳解决方案。这样的设计是为大容量存储器*设计的。
*请注意,这里重点关注内存而不是数据,因为长时间在性能至关重要的区域工作会改变您对数据类型和数据结构的看法,并查看它们如何连接到内存。在这种情况下,除非有固定分配器的帮助,否则二叉搜索树不再仅仅是对数复杂性,例如对于树节点而言可能是完全不同且对缓存不友好的内存块。该视图并没有消除算法的复杂性,但它不再依赖于内存布局。人们还开始将工作迭代视为更多关于内存访问迭代的信息。*
实际上,许多对性能至关重要的设计可以与人类易于理解和使用的高级接口设计概念非常兼容。不同之处在于,在这种情况下,“高级”将涉及内存的大容量聚合,为可能的大量数据建模的接口,并且可能是相当底层的实现。视觉上的类比可能是一辆真正舒适,易于驾驶和操纵,并且在以音速行驶时非常安全的汽车,但是如果您打开引擎盖,里面几乎没有喷火的恶魔。
采用较粗略的设计还趋向于提供更有效的锁定模式并利用代码中的并行性的更简便方法(多线程是一个详尽的主题,在这里我将跳过)。
低延迟编程的一个关键方面可能是对内存进行非常明确的控制,以提高引用的局部性以及分配和释放内存的总体速度。定制分配器池内存实际上呼应了我们所描述的相同类型的设计思想。专为散装而设计;它的设计是粗略的。它以大块的形式预分配内存,并以小块的方式池化已分配的内存。
这个想法与将昂贵的东西(例如,针对通用分配器分配内存块)推到越来越高的级别完全相同。内存池设计用于批量处理内存。
任何语言的面向对象的细粒度设计的困难之一是,它经常想引入很多用户定义的类型和数据结构。如果可以动态分配这些类型,则可以将它们分配给很小的一部分。
在C ++中,一个常见的例子是需要多态的情况,自然的诱惑是针对通用内存分配器分配子类的每个实例。
这最终将可能连续的内存布局分解为几乎没有字节的位和片段,这些位和片段分散在整个寻址范围内,从而导致更多的页面错误和高速缓存未命中。
要求最低延迟,无阻塞,确定性响应的领域可能是热点不总是归结为一个瓶颈的地方,在这里,微小的低效率实际上可以真正地“积累”(很多人想象到剖析器错误地检查了它们,但是在延迟驱动的字段中,实际上可能会出现一些罕见的情况,这些情况会导致效率低下的情况累积。造成这种累积的许多最常见原因可能是:过度分配了整个内存中的小块内存。
在Java之类的语言中,如果可能的话,对于瓶颈区域(在紧密循环中处理的区域)使用更多的普通旧数据类型数组会很有帮助,例如数组int
(但仍在庞大的高级接口后面)代替,是ArrayList
用户定义的Integer
对象。这避免了通常伴随后者的内存隔离。在C ++中,如果我们的内存分配模式高效,则不必使结构降级那么多,因为用户定义的类型可以在那里连续分配,甚至在通用容器的上下文中也可以。
此处的解决方案是为同质数据类型甚至可能跨同质数据类型提供一个自定义分配器。当微小的数据类型和数据结构在内存中展平为位和字节时,它们具有同质的性质(尽管有一些不同的对齐要求)。当我们不以内存为中心的思维方式看待它们时,编程语言的类型系统就会“想要”将潜在连续的内存区域分割/分离成很小的散乱块。
堆栈利用这种以内存为中心的焦点来避免这种情况,并有可能在其中存储用户定义类型实例的任何可能的混合组合。尽可能多地利用堆栈是一个好主意,因为堆栈的顶部几乎总是位于高速缓存行中,但是我们还可以设计内存分配器,这些内存分配器在没有LIFO模式的情况下模仿其中的某些特征,从而将不同数据类型的内存融合为连续的即使是更复杂的内存分配和释放模式也可以使用块。
当处理连续的内存块(例如,重复访问相同的缓存行,相同的页面)时,现代硬件被设计为处于顶峰。有连续性的关键字,因为这仅在周围有感兴趣的数据时才有用。因此,性能的许多关键(也是困难)是将隔离的内存块再次融合在一起,成为连续的块,这些块在逐出之前将全部访问(所有周围的数据都是相关的)。编程语言中特别是用户定义类型的丰富类型系统在这里可能是最大的障碍,但是我们总是可以在适当的时候通过自定义分配器和/或更大的设计来解决问题。
很难说“丑”。这是一个主观指标,在性能非常关键的领域工作的人会开始将其“美”的概念更改为更多面向数据的主题,并将重点放在大量处理事物的界面上。
“危险”可能会更容易。通常,性能趋向于达到较低级别的代码。例如,要实现内存分配器,就必须深入数据类型并以危险的原始位和字节级别工作,这是不可能的。结果,它可以帮助提高对这些对性能至关重要的子系统中仔细的测试过程的关注,并根据所应用的优化级别来扩展测试的彻底性。
但是所有这些都将在实现细节级别上。在经验丰富的大型和注重性能的思维定式中,“美”都倾向于转向界面设计而不是实现细节。寻找“美丽的”,可用的,安全的,有效的接口,而不是由于接口设计更改时可能发生的耦合和级联损坏而导致的实现,将成为指数级更高的优先级。实施可以随时换出。我们通常会根据需要并通过测量指出性能。界面设计的关键是在足够粗糙的水平上建模,以便为此类迭代留出空间而不会破坏整个系统。
实际上,我建议经验丰富的人将重点放在对性能至关重要的开发上,这通常会把重点放在安全性,测试,可维护性上,而通常只是SE的门徒,因为具有大量性能的大规模代码库-关键子系统(粒子系统,图像处理算法,视频处理,音频反馈,光线跟踪器,网格引擎等)将需要密切关注软件工程,以避免淹没在维护噩梦中。绝非偶然的是,最令人惊讶的高效产品通常也有最少数量的错误。
无论如何,这是我的主题,范围从真正关键性能的领域的优先事项,可以减少延迟并导致微小的效率低下的累积,以及实际上构成“美”(当以最有效的方式看待事物时)。
简单,优化性能的关键代码段
例如:
一个由5种方法组成的程序,其中3种用于数据管理,一种用于磁盘读取,另一种用于磁盘写入
这3种数据管理方法使用两种I / O方法并依赖于它们
我们将优化I / O方法。
原因: I / O方法更改的可能性较小,也不会影响应用程序的设计,总而言之,该程序中的所有内容都取决于它们,因此,它们似乎对性能至关重要,我们将使用任何代码对其进行优化。 。
这意味着我们可以获得良好的代码和易于管理的程序设计,同时通过优化代码的某些部分来保持程序的快速运行
我认为糟糕的代码会使人难以进行优化,小的错误可能会使情况更糟,因此,如果编写得好丑陋的代码,对于新手/初学者来说,好的代码会更好。