函数应该有多小?
How small should functions be?
我应该制作多小的函数?例如,如果我有一个蛋糕烘焙程序。
bakeCake(){
if(cakeType == "chocolate")
fetchIngredients("chocolate")
else
if(cakeType == "plain")
fetchIngredients("plain")
else
if(cakeType == "Red velvet")
fetchIngredients("Red Velvet")
//Rest of program
我的问题是,虽然这些东西本身很简单,但当我向 bakeCake 函数添加更多东西时,它会变得混乱。但是假设这个程序必须每秒烘烤数千个蛋糕。据我所知,与仅执行当前函数中的语句相比,使用另一个函数花费的时间要长得多(相对于计算机时间)。所以类似这样的东西应该很容易阅读,如果效率很重要,我不想把它放在那里吗?
基本上,我在什么时候为了效率而牺牲了可读性。还有一个快速的奖金问题,在什么时候拥有太多功能会降低可读性?这是 Apple 的 swift 教程示例。
func isCandyAmountAcceptable(bandMemberCount: Int, candyCount: Int) -> Bool {
return candyCount % bandMemberCount == 0
他们说因为函数名 isCandyAmountAcceptable
比 candyCount % bandMemberCount == 0
更容易阅读,所以最好为此创建一个函数。但从我的角度来看,可能需要几秒钟才能弄清楚第二个选项在说什么,但当我知道它是如何工作时它也更具可读性。
很抱歉到处都是,有点把 2 个问题合二为一。总结一下我的问题:
额外使用函数是否会影响效率(速度)?如果确实如此,我如何找出可读性和效率之间的分界线?
我应该为多小和简单的函数制作?显然,如果我必须重复该功能,我会制作它们,但是一次性使用功能怎么样?
谢谢大家,抱歉,如果这些问题是无知的,但我真的很感激你的回答。
可读性、性能和可维护性是三个不同的东西。可读性将使您的代码看起来简单易懂,不一定是最好的方法。性能始终很重要,除非您 运行 在非生产环境中编写此代码,最终结果比实现方式更重要。进入企业应用程序的世界,可维护性突然变得更加重要。你今天所做的工作将在 6 个月后移交给其他人,他们将成为 fixing/changing 你的代码。这就是标准设计模式突然变得如此重要的原因。在某种程度上,可读性是更大规模可维护性的一部分。如果上面的蛋糕烘焙程序比它看起来更复杂,那么首先突出的是代码味道是 existence if if-else。它必须被多态性取代。 switch case 类型的构造也是如此。
在什么时候你决定为另一个牺牲一个?这完全取决于您的代码实现的业务。是学术的吗?它必须是完美的解决方案,即使这意味着 90% 的开发人员乍一看都很难弄清楚到底发生了什么。它是属于零售店的网站,由来自 2 个或更多不同地理位置的 50 名开发人员组成的分布式团队维护吗?遵循传统的设计模式。
我一直看到在几乎所有情况下都遵循的一条经验法则是,如果一个函数超出了屏幕的一半,它就是重构的候选者。你有没有最终让你的编辑器有长滚动条的功能?重构!!!
Does using functions extraneously make efficiency (speed) suffer? If
it does how can I figure out what the cutoff between readability and
efficiency is?
为了性能,我通常不会考虑针对任何像样的优化器的直接函数调用的任何开销,因为它甚至可以免费提供这些功能。如果没有,在 99.9% 的情况下,它仍然是一个可以忽略不计的开销。这甚至适用于 performance-critical 字段。我在光线追踪、网格处理和图像处理等领域工作,但函数调用的成本通常排在优先级列表的底部,而不是引用的局部性、高效的数据结构、并行化和矢量化。即使你是 micro-optimizing,优先级比直接函数调用的成本要大得多,即使你是 micro-optimizing,你也经常想为你的优化器留下很多优化执行而不是试图与之抗争并手工完成所有工作(除非您实际上正在编写汇编代码)。
当然,对于某些编译器,您可能会处理那些从不内联函数调用并且对每个函数调用都有一些开销的编译器。但在那种情况下,我仍然会说它相对可以忽略不计,因为在使用这些语言和 interpreters/compilers 时,你可能不应该担心这样的 micro-level 优化。即便如此,相对而言,与提高引用位置和线程效率等更具影响力的事情相比,它也可能经常排在优先级列表的底部。
这就像您使用的编译器具有非常简单的寄存器分配,对于您使用的每个变量都有堆栈溢出,这并不意味着您应该尝试使用和重用尽可能少的变量解决它的倾向。这意味着在 non-negligible 开销的情况下使用新的编译器(例如:将一些 C 代码写入 dylib 并将其用于大多数 performance-critical 部分),或者专注于 higher-level 优化,例如使所有内容 运行 并行。
How small and simple should I make functions for? Obviously I'd make
them if I ever have to repeat the function, but what about one time
use functions?
这是我要略微讨论的地方 off-kilter 实际上建议您出于可维护性的原因考虑避免使用最微小的函数。诚然,这是一个有争议的观点,尽管至少 John Carmack 似乎有点同意(特别是关于内联代码和避免在发生副作用的情况下过度调用函数以使副作用更容易理解)。
However, if you are going to make a lot of state changes, having them
all happen inline does have advantages; you should be made constantly
aware of the full horror of what you are doing.
我认为有时在更丰富的函数方面犯错可能是好的原因是因为与简单函数相比,要理解进行更改或解决问题所需的所有信息,通常需要理解更多。
哪个更容易理解,是一个逻辑由 80 行内联代码组成的函数,还是一个分布在几十个函数中并且可能导致整个代码库中不同位置的函数?
答案不是那么明确。自然地,如果极小的函数被广泛使用,比如 sqrt
或 abs
,那么 reader 可以简单地浏览函数调用,对它的作用了如指掌.但是,如果有很多只使用一次的极小的奇异函数,那么理解整个操作的能力就需要查找它们并理解它们各自所做的事情,然后才能正确理解其中发生的事情大局方面。
我实际上不同意 Apple Swift 教程中的 one-liner 函数,因为虽然它比弄清楚算术和比较应该做什么更容易理解,但作为交换,它可能需要查看它以了解它在您不能只说 isCandyAmountAcceptable
的情况下的作用,这对我来说已经足够,并且需要弄清楚到底是什么让数量可以接受。相反,我实际上更喜欢一个简单的评论:
// Determine if candy amount is acceptable.
if (candyCount % bandMemberCount == 0)
...
... 因为这样你就不必跳转到代码中的不同位置(类比一本书将其 reader 引用到书中的其他页面导致 reader 到必须不断地在页面之间来回翻动)才能弄清楚。当然,这种 isCandyAmountAcceptable
函数背后的想法是,您不必关心关于什么让糖果数量可以接受的细节,但在实践中,我们往往不得不理解细节比我们调试代码或对其进行更改的最佳方式更频繁。如果代码永远不需要调试或更改,那么它的编写方式并不重要。对于我们所关心的,它甚至可以用二进制代码编写。但是如果它是为了维护而编写的,就像将来调试和更改一样,那么有时避免让 reader 不得不跳过很多环节是有帮助的。在这些情况下,细节通常很重要。
因此,有时将大图分割成最微小的拼图块无助于理解大图。这是一种平衡行为,但某些类型的开发人员可能会错误地将他们的系统过度分割成最细粒度的位和件并以这种方式发现维护问题。这些类型通常仍然是有前途的工程师——他们只需要找到自己的平衡点。另一个极端是编写 500 行函数并且甚至不考虑重构的人——这有点无望。但我认为你属于前一类,对你来说,我实际上建议在更丰富的函数方面犯错 ever-so-slightly 只是为了让拼图块保持健康的大小(不要太小,也不要太大)。
我什至看到了代码重复和最小化依赖之间的平衡行为。如果交换依赖于具有 800,000 行代码和关于如何使用它的史诗手册的复杂数学库,则图像库不一定会通过削减几十行重复的数学代码而变得更容易理解。在这种情况下,如果图像库选择在这里和那里复制一些数学函数以避免外部依赖,隔离其复杂性而不是将其分发到其他地方,则图像库可能更容易理解以及在新项目中使用和部署。
Basically, at what point do I sacrifice readability for efficiency.
如上所述,我不认为小图的可读性和大图的可理解性是同义词。阅读 two-line 函数并了解它的作用可能真的很容易,但距离理解您需要了解的内容以进行必要的更改还有很长的路要走。有许多这样的小 one-shot two-liners 甚至会延迟理解大局的能力。
但如果我改用 "comprehensibility vs. efficiency",我会在 design-level 处预先说明您预计会处理大量输入的情况。例如,带有自定义滤镜的视频处理应用程序知道它将在每帧上多次循环处理数百万像素。这些知识应该被用来提出一个有效的设计来重复循环数百万像素。但这是关于设计的——许多其他地方将依赖的系统的核心方面,因为大的中央设计更改成本太高,无法事后应用。
这并不意味着它必须立即开始应用 hard-to-understand SIMD 代码。这是一个实现细节,前提是设计留有足够的喘息空间来事后探索这种优化。这样的设计将意味着在 Image
级别进行抽象,在百万+像素级别,而不是在单个 IPixel
级别。这是值得提前考虑的事情。
然后,您可以优化热点,并可能使用一些 difficult-to-understand 算法和 micro-optimizations 来处理那些真正关键的情况,在这些情况下,人们强烈认为业务需要更快地进行操作,并希望手头有好的工具(分析器,即)。用户案例会指导您根据用户最常做的事情优化哪些操作,并发现他们强烈希望减少等待时间。探查器会指导您准确了解该操作中涉及的代码的哪些部分需要优化。
我应该制作多小的函数?例如,如果我有一个蛋糕烘焙程序。
bakeCake(){
if(cakeType == "chocolate")
fetchIngredients("chocolate")
else
if(cakeType == "plain")
fetchIngredients("plain")
else
if(cakeType == "Red velvet")
fetchIngredients("Red Velvet")
//Rest of program
我的问题是,虽然这些东西本身很简单,但当我向 bakeCake 函数添加更多东西时,它会变得混乱。但是假设这个程序必须每秒烘烤数千个蛋糕。据我所知,与仅执行当前函数中的语句相比,使用另一个函数花费的时间要长得多(相对于计算机时间)。所以类似这样的东西应该很容易阅读,如果效率很重要,我不想把它放在那里吗?
基本上,我在什么时候为了效率而牺牲了可读性。还有一个快速的奖金问题,在什么时候拥有太多功能会降低可读性?这是 Apple 的 swift 教程示例。
func isCandyAmountAcceptable(bandMemberCount: Int, candyCount: Int) -> Bool {
return candyCount % bandMemberCount == 0
他们说因为函数名 isCandyAmountAcceptable
比 candyCount % bandMemberCount == 0
更容易阅读,所以最好为此创建一个函数。但从我的角度来看,可能需要几秒钟才能弄清楚第二个选项在说什么,但当我知道它是如何工作时它也更具可读性。
很抱歉到处都是,有点把 2 个问题合二为一。总结一下我的问题:
额外使用函数是否会影响效率(速度)?如果确实如此,我如何找出可读性和效率之间的分界线?
我应该为多小和简单的函数制作?显然,如果我必须重复该功能,我会制作它们,但是一次性使用功能怎么样?
谢谢大家,抱歉,如果这些问题是无知的,但我真的很感激你的回答。
可读性、性能和可维护性是三个不同的东西。可读性将使您的代码看起来简单易懂,不一定是最好的方法。性能始终很重要,除非您 运行 在非生产环境中编写此代码,最终结果比实现方式更重要。进入企业应用程序的世界,可维护性突然变得更加重要。你今天所做的工作将在 6 个月后移交给其他人,他们将成为 fixing/changing 你的代码。这就是标准设计模式突然变得如此重要的原因。在某种程度上,可读性是更大规模可维护性的一部分。如果上面的蛋糕烘焙程序比它看起来更复杂,那么首先突出的是代码味道是 existence if if-else。它必须被多态性取代。 switch case 类型的构造也是如此。 在什么时候你决定为另一个牺牲一个?这完全取决于您的代码实现的业务。是学术的吗?它必须是完美的解决方案,即使这意味着 90% 的开发人员乍一看都很难弄清楚到底发生了什么。它是属于零售店的网站,由来自 2 个或更多不同地理位置的 50 名开发人员组成的分布式团队维护吗?遵循传统的设计模式。 我一直看到在几乎所有情况下都遵循的一条经验法则是,如果一个函数超出了屏幕的一半,它就是重构的候选者。你有没有最终让你的编辑器有长滚动条的功能?重构!!!
Does using functions extraneously make efficiency (speed) suffer? If it does how can I figure out what the cutoff between readability and efficiency is?
为了性能,我通常不会考虑针对任何像样的优化器的直接函数调用的任何开销,因为它甚至可以免费提供这些功能。如果没有,在 99.9% 的情况下,它仍然是一个可以忽略不计的开销。这甚至适用于 performance-critical 字段。我在光线追踪、网格处理和图像处理等领域工作,但函数调用的成本通常排在优先级列表的底部,而不是引用的局部性、高效的数据结构、并行化和矢量化。即使你是 micro-optimizing,优先级比直接函数调用的成本要大得多,即使你是 micro-optimizing,你也经常想为你的优化器留下很多优化执行而不是试图与之抗争并手工完成所有工作(除非您实际上正在编写汇编代码)。
当然,对于某些编译器,您可能会处理那些从不内联函数调用并且对每个函数调用都有一些开销的编译器。但在那种情况下,我仍然会说它相对可以忽略不计,因为在使用这些语言和 interpreters/compilers 时,你可能不应该担心这样的 micro-level 优化。即便如此,相对而言,与提高引用位置和线程效率等更具影响力的事情相比,它也可能经常排在优先级列表的底部。
这就像您使用的编译器具有非常简单的寄存器分配,对于您使用的每个变量都有堆栈溢出,这并不意味着您应该尝试使用和重用尽可能少的变量解决它的倾向。这意味着在 non-negligible 开销的情况下使用新的编译器(例如:将一些 C 代码写入 dylib 并将其用于大多数 performance-critical 部分),或者专注于 higher-level 优化,例如使所有内容 运行 并行。
How small and simple should I make functions for? Obviously I'd make them if I ever have to repeat the function, but what about one time use functions?
这是我要略微讨论的地方 off-kilter 实际上建议您出于可维护性的原因考虑避免使用最微小的函数。诚然,这是一个有争议的观点,尽管至少 John Carmack 似乎有点同意(特别是关于内联代码和避免在发生副作用的情况下过度调用函数以使副作用更容易理解)。
However, if you are going to make a lot of state changes, having them all happen inline does have advantages; you should be made constantly aware of the full horror of what you are doing.
我认为有时在更丰富的函数方面犯错可能是好的原因是因为与简单函数相比,要理解进行更改或解决问题所需的所有信息,通常需要理解更多。
哪个更容易理解,是一个逻辑由 80 行内联代码组成的函数,还是一个分布在几十个函数中并且可能导致整个代码库中不同位置的函数?
答案不是那么明确。自然地,如果极小的函数被广泛使用,比如 sqrt
或 abs
,那么 reader 可以简单地浏览函数调用,对它的作用了如指掌.但是,如果有很多只使用一次的极小的奇异函数,那么理解整个操作的能力就需要查找它们并理解它们各自所做的事情,然后才能正确理解其中发生的事情大局方面。
我实际上不同意 Apple Swift 教程中的 one-liner 函数,因为虽然它比弄清楚算术和比较应该做什么更容易理解,但作为交换,它可能需要查看它以了解它在您不能只说 isCandyAmountAcceptable
的情况下的作用,这对我来说已经足够,并且需要弄清楚到底是什么让数量可以接受。相反,我实际上更喜欢一个简单的评论:
// Determine if candy amount is acceptable.
if (candyCount % bandMemberCount == 0)
...
... 因为这样你就不必跳转到代码中的不同位置(类比一本书将其 reader 引用到书中的其他页面导致 reader 到必须不断地在页面之间来回翻动)才能弄清楚。当然,这种 isCandyAmountAcceptable
函数背后的想法是,您不必关心关于什么让糖果数量可以接受的细节,但在实践中,我们往往不得不理解细节比我们调试代码或对其进行更改的最佳方式更频繁。如果代码永远不需要调试或更改,那么它的编写方式并不重要。对于我们所关心的,它甚至可以用二进制代码编写。但是如果它是为了维护而编写的,就像将来调试和更改一样,那么有时避免让 reader 不得不跳过很多环节是有帮助的。在这些情况下,细节通常很重要。
因此,有时将大图分割成最微小的拼图块无助于理解大图。这是一种平衡行为,但某些类型的开发人员可能会错误地将他们的系统过度分割成最细粒度的位和件并以这种方式发现维护问题。这些类型通常仍然是有前途的工程师——他们只需要找到自己的平衡点。另一个极端是编写 500 行函数并且甚至不考虑重构的人——这有点无望。但我认为你属于前一类,对你来说,我实际上建议在更丰富的函数方面犯错 ever-so-slightly 只是为了让拼图块保持健康的大小(不要太小,也不要太大)。
我什至看到了代码重复和最小化依赖之间的平衡行为。如果交换依赖于具有 800,000 行代码和关于如何使用它的史诗手册的复杂数学库,则图像库不一定会通过削减几十行重复的数学代码而变得更容易理解。在这种情况下,如果图像库选择在这里和那里复制一些数学函数以避免外部依赖,隔离其复杂性而不是将其分发到其他地方,则图像库可能更容易理解以及在新项目中使用和部署。
Basically, at what point do I sacrifice readability for efficiency.
如上所述,我不认为小图的可读性和大图的可理解性是同义词。阅读 two-line 函数并了解它的作用可能真的很容易,但距离理解您需要了解的内容以进行必要的更改还有很长的路要走。有许多这样的小 one-shot two-liners 甚至会延迟理解大局的能力。
但如果我改用 "comprehensibility vs. efficiency",我会在 design-level 处预先说明您预计会处理大量输入的情况。例如,带有自定义滤镜的视频处理应用程序知道它将在每帧上多次循环处理数百万像素。这些知识应该被用来提出一个有效的设计来重复循环数百万像素。但这是关于设计的——许多其他地方将依赖的系统的核心方面,因为大的中央设计更改成本太高,无法事后应用。
这并不意味着它必须立即开始应用 hard-to-understand SIMD 代码。这是一个实现细节,前提是设计留有足够的喘息空间来事后探索这种优化。这样的设计将意味着在 Image
级别进行抽象,在百万+像素级别,而不是在单个 IPixel
级别。这是值得提前考虑的事情。
然后,您可以优化热点,并可能使用一些 difficult-to-understand 算法和 micro-optimizations 来处理那些真正关键的情况,在这些情况下,人们强烈认为业务需要更快地进行操作,并希望手头有好的工具(分析器,即)。用户案例会指导您根据用户最常做的事情优化哪些操作,并发现他们强烈希望减少等待时间。探查器会指导您准确了解该操作中涉及的代码的哪些部分需要优化。