入门级工程师有关内存管理的问题


9

自从我担任入门级软件开发人员职位以来已经过去了几个月。现在,我已经克服了一些学习上的困难(例如,语言,行话,VB和C#的语法),我开始专注于更深奥的主题,以编写更好的软件。

我向同事提出的一个简单问题是“我专注于错误的事情”。尽管我尊重这位同事,但我不同意这是要重点关注的“错误”。

这是代码(在VB中),然后是问题。

注意:函数GenerateAlert()返回一个整数。

Dim alertID as Integer = GenerateAlert()
_errorDictionary.Add(argErrorID, NewErrorInfo(Now(), alertID))    

与...

 _errorDictionary.Add(argErrorID, New ErrorInfo(Now(), GenerateAlert()))

我最初编写了后者,并用“ Dim alertID”将其重写,以便其他人可能会更容易阅读。但是,这是我的关注和问题:

如果使用Dim AlertID编写此消息,则实际上会占用更多内存。有限但更多,是否应该多次调用此方法,否则会导致问题?.NET将如何处理此对象AlertID。在.NET外部,应该在使用后手动处理对象(在子程序末尾附近)。

我想确保我成为一个知识渊博的程序员,而不仅仅是依靠垃圾回收。我在想这个吗?我是否专注于错误的事情?


1
我可以很容易地断定他是100%,因为第一个版本更具可读性。我打赌编译器甚至可能会照顾您所关心的问题。即使没有,您也过早地进行了优化。
钻机2012年

6
我完全不确定使用匿名整数还是使用命名整数会更多地使用内存。无论如何,这确实是过早的优化。如果您需要担心此级别的效率(我几乎可以肯定不会),则可能需要C ++而不是C#。理解性能问题以及幕后发生的事情是件好事,但这是一棵大森林中的一棵小树。
psr

5
命名vs匿名整数不会使用更多的内存,尤其是因为匿名整数只是您没有命名的命名整数(编译器仍然必须命名)。命名的整数最多将具有不同的作用域,因此它的寿命可能更长。匿名整数仅在方法需要时才能使用,命名整数将在其父级需要时使用。
乔尔·埃瑟顿

让我们看看...如果Integer是一个类,它将在堆上分配。局部变量(最有可能在堆栈上)将保存对它的引用。该引用将传递给对象errorDictionary。如果运行时正在执行引用计数(或诸如此类),那么当没有更多引用时,它将从堆中释放它(对象)。方法退出后,堆栈中的所有内容都会自动“取消分配”。如果它是原始的,它将(很可能)最终出现在堆栈中。
保罗

您的同事是对的:您的问题提出的问题不应该是关于优化的问题,而应该是关于可读性的问题
haylem 2012年

Answers:


25

“过早的优化是编程中所有邪恶(或至少是大多数邪恶)的根源。” -唐纳德​​·努斯

第一次使用时,只需编写代码,使其正确无误即可。如果以后确定您的代码对性能至关重要(可以使用一些工具来确定此性能,则称为探查器),则可以对其进行重写。如果您的代码未确定对性能至关重要,那么可读性就更为重要。

是否值得深入研究性能和优化的这些主题?绝对可以,但是如果不需要的话,也可以不以公司的美元为准。


1
应该用谁的美元呢?您的雇主比您从技能提高中受益更多。
Marcin 2012年

不会直接对您当前任务有所帮助的主题?您应该自己从事这些事情。如果我坐下来研究所有引起我好奇心的CompSci项目,我将一事无成。那就是我晚上的目的。
Christopher Berman

2
奇怪的。我们中有些人的生活很私人,正如我所说,雇主主要从这项研究中受益。关键是不要花一整天的时间在上面。
Marcin 2012年

6
对你有益。但是,这实际上并没有使其成为通用规则。另外,如果您不鼓励员工学习工作,那么您要做的就是劝阻他们学习,并鼓励他们找到另一位实际为员工发展付费的雇主。
Marcin 2012年

2
我理解上述评论中提到的观点;我想指出我午休时问的问题。:)。再次感谢大家在这里以及整个Stack Exchange网站上的投入;这是无价的!
肖恩·霍布斯

5

对于您的普通.NET程序,是的,它想得太多。有时您可能想深入了解.NET内部到底发生了什么,但这种情况很少见。

我遇到的困难转变之一是从使用C和MASM切换到90年代经典VB中的编程。我习惯于对大小和速度进行优化。我不得不在很大程度上放弃了这种想法,而让VB这样做才是有效的。


5

正如我的同事经常说的:

  1. 让它起作用
  2. 修复所有错误,使其正常工作
  3. 使其坚固
  4. 如果执行缓慢,请套用最佳化

换句话说,请始终牢记KISS(保持简单愚蠢)。由于过度设计,过度思考某些代码逻辑可能是下次更改逻辑的问题。但是,保持代码整洁和简单始终是一种好习惯

但是,通过时间和经验,您会更好地了解哪些代码有气味,并且很快需要进行优化。


3

应该用Dim AlertID编写它吗

可读性很重要。在你的榜样,虽然,我不知道,你实拍的东西更易读。GenerateAlert()有一个好名字,并且不会增加很多噪音。时间可能有更好的利用。

实际上它将占用更多的内存;

我怀疑不是。对于编译器而言,这是一个相对简单的优化。

应该多次调用此方法是否会导致问题?

使用局部变量作为中介不会对垃圾收集器产生任何影响。如果GenerateAlert()新增了up的内存,那将很重要。但是,无论是否存在局部变量,这都将很重要。

.NET将如何处理此对象AlertID。

AlertID不是对象。GenerateAlert()的结果是对象。AlertID是变量,如果它是局部变量,则该变量只是与方法相关联的用于跟踪事物的空间。

在.NET外部,应在使用后手动处置对象

这是一个棘手的问题,具体取决于所涉及的上下文和GenerateAlert()提供的实例的所有权语义。通常,无论创建了什么实例,都应将其删除。如果您在设计程序时考虑了手动内存管理,则您的程序可能看起来会大不相同。

我想确保我成为一个知识渊博的程序员,而不仅仅是依靠垃圾回收。我在想这个吗?我是否专注于错误的事情?

一个好的程序员使用他们可用的工具,包括垃圾收集器。思索事物总比忘却生活好。您可能专注于错误的事情,但是由于我们在这里,您不妨了解一下。


2

使它正常工作,使其清洁,使其坚固,然后使其按需要工作的速度运行

那应该是正常的事情。您的首要任务是制造出可以通过验收测试的产品,从而超出要求。这是您的第一要务,因为这是客户的第一要务。在开发期限内满足功能要求。下一个优先事项是编写清晰易读的代码,这些代码易于理解,因此在必要时可以由后代维护,而无需任何WTF(这几乎绝不是“ if”的问题;您或您之后的任何人都必须去)返回并更改/修复某些内容)。第三要务是使代码遵循SOLID方法(如果需要,可以使用GRASP),该方法将代码放入模块化,可重用,可替换的块中,这再次有助于维护(他们不仅可以了解您的工作以及原因,但是我可以通过外科手术删除并替换部分代码。最后的优先考虑是性能。如果代码足够重要以至于必须符合性能规格,那么几乎可以肯定它就足够重要,因此必须首先使其正确,整洁并SOLID。

呼应克里斯托弗(和唐纳德·纳努斯(Donald Knuth)),“过早的优化是万恶之源”。此外,您正在考虑的优化类型都是次要的(无论是否在源代码中给它一个名称,都会在堆栈上创建对新对象的引用),并且其类型不会对编译产生任何影响白介素 变量名称不会保留到IL中,因此,由于您是在变量首次使用(并且可能仅在使用之前)就声明了变量,因此我敢打赌,两个示例之间的IL是相同的。因此,您的同事是100%正确的;如果您在寻找命名变量与内联实例化来优化的东西,那么您就在错误的位置。

.NET中的微优化几乎是不值得的(我说的是99.99%的情况)。在C / C ++中,如果您知道自己在做什么,也许吧。在.NET环境中工作时,您与硬件的距离已经足够远,以至于代码执行会产生大量开销。因此,假设您已经处在一个表明您已经放弃了超快速度的环境中,而是在寻找编写“正确的”代码的环境,那么如果.NET环境中的某些功能确实不能足够快地运行,那么其复杂性就是太高,否则您应该考虑并行化它。这里有一些优化的基本指示。我向您保证,您的优化工作效率(所花时间的速度)将飞速增长:

  • 更改函数形状比更改系数更重要 -WRT Big-Oh复杂度,您可以将必须​​在N 2算法中执行的步骤数减少一半,并且即使它执行时,仍然具有二次复杂度算法过去的一半时间。如果这是此类问题的复杂性下限,那就可以了,但是如果对同一问题有NlogN,线性或对数解决方案,那么通过切换算法来降低复杂性而不是通过优化算法来获得更大的收益。
  • 仅仅因为你看不到的复杂性并不意味着它不会花费你 -许多单词中最优雅的单行执行可怕(例如,正则表达式主要检查是一个指数,复杂的功能,而高效涉及将数除以小于其平方根的所有素数的素数评估约为O(Nlog(sqrt(N)))。Linq是一个很棒的库,因为它简化了代码,但与SQL引擎不同,.Net编译器不会尝试找到执行查询的最有效方法,您必须知道使用方法时会发生什么,因此为什么在生成时将方法放在链中的较早(或更晚)会更快。相同的结果。
  • OTOH,在源代码复杂度和运行时复杂度之间几乎总是要权衡取舍 – SelectionSort非常易于实现;您大概可以在10LOC或更少的时间内完成。MergeSort更复杂一些,Quicksort更加复杂,而RadixSort更加复杂。但是,随着算法的编码复杂度增加(从而增加了“前期”开发时间),它们的运行时复杂度也随之降低。MergeSort和QuickSort是NlogN,而RadixSort通常被认为是线性的(从技术上讲,它是NlogM,其中M是N中最大的数字)。
  • 快点休息 -如果有一张可以廉价进行的支票,很可能是真的,这意味着您可以继续前进,请先进行支票。例如,如果您的算法仅关心以1、2或3结尾的数字,则最有可能的情况(根据完全随机的数据)是以某个其他数字结尾的数字,因此请测试该数字不以结尾在检查数字是否以1、2或3结尾之前,先检查1、2或3。如果逻辑需要A&B,且P(A)= 0.9而P(B)= 0.1,则检查首先是B,除非!A然后是!B(如if(myObject != null && myObject.someProperty == 1)),否则B花费比A多9倍以上的时间来评估(if(myObject != null && some10SecondMethodReturningBool()))。
  • 不要问您已经知道答案的任何问题 -如果您有一系列“跌倒”条件,并且其中一个或多个条件取决于必须同时检查的较简单条件,则不要同时检查这两个条件这些都是独立的。例如,如果您有一张要求A的支票,而一张要求A && B的支票,则应选中A,如果为true,则应选中B。如果!A,则!A && B,因此也不必理会。
  • 您做某事的次数越多,您就应该越注意它的完成方式 -这是开发中的一个常见主题,在许多层面上都如此;从一般的发展意义上讲,“如果一项常见的任务既费时又费力,那就继续做下去,直到您既沮丧又有足够的知识以找到更好的方法为止”。用代码术语来说,运行效率低下的算法的次数越多,通过对其进行优化就可以获得越多的总体性能。有一些分析工具可以采用二进制程序集及其调试符号,并在经历了一些用例后向您显示运行最多的代码行。这些线以及运行这些线的线是您最应该注意的,因为您所实现的效率提高都会成倍增加。
  • 如果您向其投放足够的硬件,则较复杂的算法看起来较不复杂。在某些情况下,您只需要意识到您的算法已接近运行它的系统(或其一部分)的技术极限;从那时起,如果需要更快,只需在更好的硬件上运行它,您将获得更多收益。这也适用于并行化。当在N个核上运行时,N 2复杂度算法看起来是线性的。因此,如果您确定自己已达到所写算法类型的较低复杂性界限,请寻找“分而治之”的方法。
  • 只要足够快,它就会很快 -除非您是手工组装以特定芯片为目标的组件,否则总会有一些收获。但是,除非希望手动组装,否则必须始终牢记客户所说的“足够好”。同样,“过早的优化是万恶之源”;当您的客户称呼它足够快时,您就完成了,直到他认为不再足够快为止。

0

尽早担心优化的唯一时间是,当您知道要处理的东西很大时,或者您知道将要执行很多次时。

“巨大”的定义显然会根据目标系统的不同而有所不同。


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.