Java中的模板“元编程”是个好主意吗?


29

在一个相当大的项目中有一个源文件,它具有几个对性能极为敏感的功能(每秒被称为百万次)。实际上,先前的维护者决定编写一个函数的12个副本,每个副本的差别都很小,以节省在单个函数中检查条件的时间。

不幸的是,这意味着代码是要维护的PITA。我想删除所有重复的代码,只写一个模板。但是,Java语言不支持模板,我不确定泛型是否适合于此。

我目前的计划是写一个文件,该文件生成该函数的12个副本(实际上是一个只能使用的模板扩展器)。对于为什么必须以编程方式生成文件,我当然会提供很多解释。

我担心的是,这将导致将来的维护人员感到困惑,并且如果他们在修改文件后忘记重新生成文件,或者(如果更糟糕的话)他们以编程方式生成的文件进行修改,则可能会引入讨厌的错误。不幸的是,除了没有用C ++重写整个过程外,我看不到任何解决方法。

这种方法的好处是否大于缺点?我应该改为:

  • 发挥性能优势,并使用单个可维护的功能。
  • 添加有关为何必须重复执行该功能12次的说明,并应承担维护负担。
  • 尝试将泛型用作模板(它们可能无法那样工作)。
  • 大吼大叫的老维护者,使代码的性能取决于单个函数。
  • 其他保持性能和可维护性的方法?

PS:由于该项目的设计不佳,对功能进行性能分析非常棘手……但是,这位前维护人员说服了我,性能下降是不可接受的。我认为他的意思是超过5%,尽管这完全是我的猜测。


也许我应该详细说明一下。这12份副本执行的任务非常相似,但差别很小。在整个函数中的不同地方存在差异,因此不幸的是,有很多很多条件语句。有效地有6种“操作模式”和2种“范例”(由我自己编造的词)。要使用该功能,可以指定操作的“模式”和“范例”。这从来都不是动态的。每段代码仅使用一种模式和范例。所有12个模式范例对都在应用程序中的某个位置使用。这些函数恰当地命名为func1到func12,偶数代表第二个范例,奇数代表第一个范例。

我知道,如果以可维护性为目标,那将是最糟糕的设计。但是它似乎“足够快”,并且这段代码已经有一段时间不需要任何更改了。也值得注意的是原始功能尚未删除(尽管据我所知这是无效代码) ,因此重构将很简单。


如果性能影响严重到足以保证该功能有12个版本,那么您可能会对此感到困惑。如果您重构为单个功能(或使用泛型),那么性能下降是否会严重到足以失去客户和业务的地步?
FrustratedWithFormsDesigner 2014年

12
然后,我认为您需要进行自己的测试(我知道您说概要分析很棘手,但是您仍然可以对整个系统进行粗略的“黑匣子”性能测试,对吗?),看看两者之间的差别有多大是。而且如果值得注意的话,那么我认为您可能会被代码生成器所困扰。
FrustratedWithFormsDesigner 2014年

1
这听起来好像可以从某种参数多态性(甚至是泛型)中受益,而不是使用不同的名称来调用每个函数。
罗伯特·哈维

2
给函数func1 ... func12命名似乎很疯狂。至少将它们命名为mode1Par2等...或myFuncM3P2。
user949300 2014年

2
“如果他们修改了以编程方式生成的文件,则...”仅在从“ Makefile”(或您使用的任何系统)构建时创建文件,然后在编译完成后将其删除。这样,他们根本没有机会修改错误的源文件。
Bakuriu 2014年

Answers:


23

这是一个非常糟糕的情况,您需要尽快重构此代码-最坏的情况是技术负担-您甚至不知道代码的真正重要性-仅推测它很重要。

关于解决方案:
可以做的事情是添加自定义编译步骤。如果使用实际上非常简单的Maven,则其他自动化构建系统也可能会对此进行处理。编写一个扩展名与.java 不同的文件,并添加一个自定义步骤,在您的源代码中搜索类似的文件,然后重新生成实际的.java。您可能还想在自动生成的文件上添加一个巨大的免责声明,说明不对其进行修改。

赞成还是反对使用一次生成的文件:您的开发人员将无法对.java进行更改。如果他们确实在提交之前在自己的计算机上运行代码,他们将发现所做的更改无效(哈哈)。然后也许他们会阅读免责声明。记住必须以不同的方式更改此特定文件,因此绝对不信任团队成员和未来的自己是绝对正确的。它还允许自动测试,因为JUnit会在运行测试之前编译程序(并重新生成文件)。

编辑

根据评论判断,答案就好像是无限期进行这项工作的一种方式,也许可以将其部署到项目的其他性能关键部分。

简单地说:不是。

从长远来看,创建自己的迷你语言,为其编写代码生成器并进行维护的额外负担,更不用说将其教给未来的维护者了。当您使用长期解决方案时,以上仅提供了一种更安全的方式来解决问题。那将是我之上的。


19
抱歉,但我不同意。您对OP的代码了解不足,无法为他做出此决定。在我看来,生成代码的代码似乎比原始解决方案包含更多的技术债务。
罗伯特·哈维

6
罗伯特的评论不能夸大其词。任何时候只要您有“免责声明解释不修改文件”,即等同于由昵称为“鲨鱼”的非法博彩公司经营的支票现金商店的“技术债务”。
corsiKa 2014年

8
自动生成的文件当然不属于源存储库(毕竟它不是源文件,毕竟是编译后的代码),并且应该以ant clean任何方式将其删除。无需在不存在的文件中放入免责声明!
约尔格W¯¯米塔格

2
@RobertHarvey我没有为OP做出任何决定。OP询问以他的方式拥有这样的模板是否是一个好主意,我提出了一种(更好)的方式来维护它们。这不是一个理想的解决方案,实际上,正如我在第一句中所说的那样,整个局面很糟糕,一种更好的解决方法是消除问题,而不是做出不稳定的解决方案。这包括正确评估此代码的关键程度,对性能的影响有多严重以及是否有办法使它在不编写大量错误代码的情况下工作。
Ordous

2
@corsiKa我从未说过这是一个持久的解决方案。这将与原始情况一样多。它唯一要做的就是降低波动率,直到找到系统的解决方案为止。这可能包括完全迁移到新的平台/框架/语言,因为如果您遇到Java中的此类问题-您正在做的事情非常复杂或极其错误。
2014年

16

维护工作是真的吗,还是打扰您了?如果它只打扰您,那就别管它了。

性能问题是真的吗,还是以前的开发人员只是认为是真的?即使使用了探查器,性能问题也常常不被认为在哪里。(我使用这种技术来可靠地找到它们。)

因此,您有可能对一个非问题有一个丑陋的解决方案,但对一个实际问题也可能是一个丑陋的解决方案。如有疑问,请不要理会。


10

真正确保比在实际生产条件下(实际内存消耗,实际触发垃圾回收等),所有这些单独的方法确实在性能上有所不同(而不是只有一种方法)。这将花费几天的时间,但是通过简化从现在开始使用的代码,可以节省数周的时间。

另外,如果确实发现需要所有功能,则可能需要使用Javassist以编程方式生成代码。正如其他人指出的那样,您可以使用Maven将其自动化。


2
同样,即使事实证明性能问题是真实的,研究它们的练习也将帮助您更好地了解代码可能带来的影响。
Carl Manaster 2014年

6

为什么不为该系统包括某种模板预处理器\代码生成?一个定制的额外构建步骤,在编译其余代码之前,该步骤将执行并发出额外的Java源文件。这就是包括wsdl和xsd文件的Web客户端经常工作的方式。

当然,您将拥有该预处理器\代码生成器的维护,但是不必担心重复的核心代码维护。

由于发出了编译时Java代码,因此无需为额外代码付出任何性能损失。但是,通过拥有模板而不是所有重复的代码,可以简化维护工作。

由于Java语言中的类型擦除,Java泛型没有任何性能上的好处,因此使用了简单的强制转换。

根据我对C ++模板的了解,每个模板调用都会将它们编译成多个函数。例如,对于存储在std :: vector中的每种类型,您将获得重复的中间编译时间代码。


2

没有理智的Java方法足够长,无法拥有12个变体...而且JITC讨厌长方法-它只是拒绝适当地优化它们。我已经看到通过将方法简单地分为两个较短的方法就可以将速度提高两倍。也许这是要走的路。

即使存在相同的副本,具有多个副本的OTOH也可能有意义。随着它们在不同地方的使用,它们针对不同的情况进行了优化(JITC对它们进行了分析,然后将罕见的情况放在一条例外的道路上。

我想假设有充分的理由,生成代码没什么大不了的。开销很低,并且正确命名的文件会立即指向其源。很久以前,当我生成源代码时,我把// DO NOT EDIT每一行都放上了……我想这已经足够保存了。


1

您提到的“问题”绝对没有错。据我所知,这是数据库服务器用来获得良好性能的确切设计和方法。

他们有许多特殊的方法来确保它们可以在各种条件下最大化各种操作的性能:联接,选择,聚合等。

简而言之,像您认为的那样生成代码不是一个好主意。也许您应该查看此图,看看数据库如何解决与您类似的问题:在此处输入图片说明


1

您可能想检查是否仍然可以使用一些明智的抽象方法,并使用例如模板方法模式为通用功能编写可理解的代码,并将方法之间的差异移至“原始操作”(根据模式描述),参见图12。子类。这可能会改善可维护性和可测试性,并且实际上可能具有与当前代码相同的性能,因为JVM可以在一段时间后内联用于原始操作的方法调用。当然,您必须在性能测试中进行检查。


0

您可以使用Scala语言解决专用方法的问题。

Scala可以内联方法,这种方法(结合简单的高阶函数用法)使得可以免费避免代码重复-这看起来像您答案中提到的主要问题。

而且,Scala具有语法宏,这使得在编译时以类型安全的方式对代码执行很多操作成为可能。

当在泛型中使用装箱基元类型的常见问题也可以在Scala中解决:它可以对基元进行泛化专门化,从而避免使用@specialized注释自动装箱-这是语言本身内置的东西。因此,基本上,您将在Scala中编写一种通用方法,它将以专用方法的速度运行。而且,如果您还需要快速的泛型算术,则可以通过使用Typeclass“模式”为不同的数字类型注入运算和值来轻松实现。

另外,Scala在许多其他方面都很棒。而且您不必将所有代码重写为Scala,因为Scala <-> Java的互操作性非常好。只要确保使用SBT(scala构建工具)来构建项目即可。


-1

如果所讨论的函数很大,则将“模式/范例”位转换为接口,然后将实现该接口作为参数的对象传递给函数即可。GoF将其称为“策略”模式iirc。如果函数很小,增加的开销可能会很大。有人有人提到概要分析了吗?...更多的人应该提及概要分析。


这更像是评论,而不是答案
gna

-2

该程序多大了?最有可能是较新的硬件将消除瓶颈,您可以切换到更易维护的版本。但是有必要进行维护,因此,除非您的任务是改善代码库,否则请使其保持正常运行。


1
这似乎只是在重复几小时前发布的先前答案中提出和解释的观点
gnat 2014年

1
确定瓶颈实际上在哪里的唯一方法是剖析瓶颈-如其他答案所述。没有这些关键信息,对性能的任何建议修复都是纯粹的推测。
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.