Java enumset的成本 - 对生态系统的批评

2021-06-12 07:44:44

自从我上次在Java工作以来,已经大约十年了,很多已经改变了。我以为Joshua Blochwhoich以来,我认为我已经收到了一个新版本。曾经是我的指导星星队。然而,在各种语言和平台的额外经验之后,这本书经常让我摇头。不同意其20%的物品。一个冲突oppic是枚举,特别是第36项:“使用enumset而不是位字段。”

这并不是说Bloch必然是错误的,或者我不喜欢的任何其他建议。这本书是保守的,在java成语的一侧跳动而不是java成语,无论是mainessense的何种误乱,都在玩itsafe。问题在于Java Idiom而不是这本书。写下你的大家都像其他人一样,正如书所说的那样。在罗马做到入乡随俗。

另一方面,enumset可能是javastandard库中最无意义的类。它有两个目标,但两者都失败了。

返回旧时代Java程序员将像Cprogramer一样建立枚举。例如,这是一个C样式位阵容(TheSeromans爱他们的关键字):

公共最终类颜色{公共静态Final int r = 1<< 0;公共静态Final int g = 1<< 1;公共静态Final int B = 1<<< 2; }

要构建一组这些项目,请使用或(|)运算符,就像C:

缺点是缺乏类型的安全性。这些是普通的旧整数,而不是暗示的类型,并且越过越过的整数和位字段不会被编译器接受。要解决此问题,早期的Java获得了枚举型式:

值得庆幸的是,这只是比狭衰减的同样的语法稍微冗长。虽然它是有用的,但这些类型不支持或运营商。相反,你应该建立一套。罗马人也喜欢:

与您可能猜到的一样,与原始整数操作相比,ThisHashSet非常慢,效率效率令人难以置信。类型的安全性具有成本的成本。 Java尝试缓解此项,为枚举提供特殊的SetImplationation:

没有像或操作员那样句法倾斜,而是比哈希集更少的仪式和莫雷夫特。效率来自使用BitFieldInternally,就像原始预枚举示例一样。但是有多么高效?

原始的C型位菲尔德是一个原始的INT:快速,高效,清除,易于优化。除了缺乏类型的安全性之外,它是最好的案例。由于enumset使用BitFieldInternally,因此它不是基本相同的吗?不幸的是没有。

这是个人元素思考的本质。有Littlereason认为忠诚将是高效的。

想要了解相对成本的感觉,我汇集了一些粗壮的标志。在基准时,我构建一组值,然后构造相同的集合了多次并将其与原始集进行比较。这是enumset基准:

枚举标志{a,b,c,d,e,f,g} // ...静态void benchcharkenumset(){system。 GC ();长乞求=系统。 nanotime();设置<旗帜> a = enumset。 (旗帜。一个,国旗。B,旗帜。G); for(int i = 0; i< 1_000_000_000; i ++){set<旗帜> b = enumset。 (旗帜。一个,国旗。B,旗帜。G);断言一个。等于(b);长尾=系统。 nanotime();系统 。出去 。 println(" enumset \ t" +(最终 - 乞讨)/ 1 e9); }

静态int a = 1&lt;&lt; 0;静态最终int b = 1&lt;&lt; 1;静态最终int c = 1 <&lt;&lt; 2;静态最终int d = 1&lt;&lt; 3;静态int e = 1&lt;&lt; 4;静态最终int f = 1&lt;&lt;&lt; 5;静态Final int g = 1 <&lt;&lt;&lt; 6; // ...静态void benchmarkbitfield(){system。 GC ();长乞求=系统。 nanotime(); int a = a | B | G ; for(int i = 0; i&lt; 1_000_000_000; i ++){int b = a | B | G ;断言A == B;长尾=系统。 nanotime();系统 。出去 。 println(&#34;位菲尔德\ t&#34; +(最终 - 乞讨)/ 1 e9); }

还有一个散列基准,但它只是Theenumset基准的略有变化,所以我不会在这里展示。由于JIT预热成本,在相同的过程中,Benchmark在相同的过程中连续运行三次(for循环)。 -ea选项使测试中的断言是:

X86-64 Debian Buster的结果及其OpenJDK 11(OpenJDK最新的支持版本):

enumset是比哈希集快的两个数量级。这个Soundspretty直到下一个结果:在他们最糟糕的情况下,比特菲尔德比enumset更快的速度率快。

第三次运行更有趣的是更有趣的。它看起来像个基准文件,在另一个背景下,我可能会同意。显然,JIT编译并优化了整个基准。通常这是不可避免的结果,但它与其他TwoBenchmark相比之下。编译器未能实现相同的优化,其中哈希集和enumset基准测试。一旦加热,位菲尔德将超过1000倍,因为它们不会抑制优化。

那么enumset的点是什么?我提到它并没有实现其两个目标。

通常的设置实现,散列效率远远低于Abitfield。 enumset试图将其与abitfield一起键入。

由于已经是一组Propertyof,因此不需要枚举。我们已经有一个更常规的设置实现:hashset.Relive对位域,enumset比ahashset更快地有意义。一个野兔没有比乌龟在上下文中的乌龟更快。

对位菲尔德的常见争论是“速度并不重要”,否则它是“过早优化”。如果这是真的,那么哈希集是什么?如果速度很重要,那么你使用位域。在哪里需要枚举?它是非态度的额外API表面积。

它也与“第6项:避免创建不必要的对象。”与集合不同,位域是一个原始的,没有创建对象。截至基准,一套的类型安全性高成本。我内心的CPRIGHMAGMER甚至在那里的成本中畏缩,即使它真正没有经过。

这并不是说你应该改变你写Java的方式。这是生态系统的班班主义 - 它的设计和习语 - 以及我没有错过的(多种)原因之一。

对这篇文章有评论吗?通过向~skeeto/[email protected] [邮件列表礼仪]发送电子邮件,或查看现有讨论,在我的公共收件箱中开始讨论。