写点什么

Java 社区的一次十亿行数据编程挑战

  • 2025-03-13
    北京
  • 本文字数:9616 字

    阅读完需:约 32 分钟

大小:4.56M时长:26:32
Java 社区的一次十亿行数据编程挑战

Gunnar Morling 是一位软件工程师和开源爱好者,目前在 Decodable 从事基于 Apache Flink 的流处理工作。之前他在 Redhat 领导了 Debezium 项目。他是 Java Champion,创立了多个开源项目,如 JfrUnit、kcctl 和 MapStruct。Gunnar 曾在 QCon、Java One 和 Devoxx 等各种会议上发表过演讲。本文中,Gunnar Morling 讨论了应对一个包含十亿行数据的文件时,速度最快的解决方案所采用的一些技巧,这些技巧通过并行化和高效的内存访问在不到两秒的时间内就能处理 13 GB 的输入文件。

 

我想谈谈我之前参加的一个病毒式编程挑战,名为“十亿行挑战”(1 Billion Row Challenge,1BRC)。



我在一家名为 Decodable 的公司担任软件工程师。我们基于 Apache Flink 构建了一个用于流处理的托管平台。这件事对我来说完全是副业,但 Decodable 支持我这样做。

 

之所以举办这项挑战是因为我想学点新东西。每六个月就会有新的 Java 版本出现,带有新的 API 和新功能。要跟踪所有这些开发成果并非易事。我想知道这些新 Java 版本中有哪些新东西,我能用它们做什么?同时我也想为社区提供一个渠道让大家都能学到新东西。在这个挑战中,你可以从其他人的实现里汲取灵感。另外我还想要纠正一个偏见,那就是很多人认为 Java 很慢。但如果你看看现代版本及其特性,你会发现这完全是错误的。

挑战内容

我们先来深入研究一下挑战的细节。

 

我们的想法是,有一个包含温度测量值的文件,本质上就像一个 CSV,但它不是以逗号,而是以分号作为分隔符。内容有两列,还有一个车站名称,如汉堡或慕尼黑等。与之关联的温度测量值是随机值。

 

挑战任务是处理该文件,汇总文件中的值,并为每个站点确定最小值、最大值和平均温度值,很简单。唯一的警告是,正如挑战的名称所示,它有 10 亿行,生成文件的大小约为 13 GB,相当大。然后你必须打印出结果,如上图。

规则

关于规则再多说一点。首先,这主要针对 Java。为什么?因为这是我最了解的平台,我想支持它,想传播关于它的知识。然后,你可以选择任何版本,新版本、预览版本、所有类型的发行版,如 GraalVM,或各种各样的 JDK 提供程序都可以。你可以使用一个名为 SDKMAN 的工具进行管理。这是一个非常好的工具,可以管理很多 Java 版本,并在不同版本之间来回切换。



仅限 Java,没有依赖项。引入一些库为你完成任务没什么意义,你应该自己编程。各次运行之间没有缓存。我对每个实现都运行了五次。然后我丢弃了最快和最慢的那个,并从剩下的三次运行结果中取平均值。可以缓存的话,你只需执行一次任务,将结果保存在文件中,然后将其读回,这样就会非常快。这没有多大意义。你也可以从他人那里获得灵感。

评估环境

关于我的运行环境:

我的公司花了 100 欧元购买了这台机器,有 32 个内核,其中我主要只使用 8 个内核。有相当多的 RAM。实际上文件总是从 RAM 盘读取。我想确保磁盘 I/O 不至于影响性能,因为它的可预测性要差得多。这里只有一个纯粹的 CPU 绑定问题。



上面是我的基本实现。我使用这个 Java Streams API、这个 files.lines 方法,它为我提供了一个包含文件行的流。我从磁盘读取该文件,然后使用 split 方法将每行映射到那里。我想将站名与值分开。然后我将结果(行)收集到这个分组收集器中。我按站名对其分组。

 

然后,对于我的每个站,我需要聚合这些值,在我的聚合器实现中处理。每当新值添加到现有聚合器对象时,我都会跟踪最小值和最大值。为了计算平均值,我会跟踪值的总和和计数。非常简单。然后,如果我并行运行这个操作,我需要合并两个聚合器,同样非常简单。最后,如果我完成了,需要减少处理结果,然后通过对象发出这样的结果,其中包含最小值和最大值。对于平均值,我只需将总数除以计数,然后将其打印出来。在这台机器上这大约需要五分钟,不算超级快,但也不是很糟糕。编写这段代码花了我半个小时不到,还不错。如果你在工作中解决这个问题,到这里你可能会收工回家,喝杯咖啡,就完事了。当然,为了这次挑战的目的,我们希望速度更快,看看我们能在这里取得多大的进展。

第一个提交

这项挑战重要的是必须有人来参与。一位来自荷兰的 Java 冠军 Roy Van Rijn 立刻对此产生了兴趣,在我发布帖子后大约一小时,他创建了自己的第一个实现,不是很花哨或很复杂。只要第一个人出现,其他人也会来参与。

并行化

我们深入了解一下如何加快这个程序的速度。人们花了整个一月份的时间来研究这个问题,他们探索到了一个非常深的层次,基本上是计算 CPU 指令。

 

首先我们谈谈并行化,因为我们有很多 CPU 核心。在我用来评估它的服务器上有 32 个核心,64 个线程。我们想利用这一点,只使用一个核心会有点浪费。我们该怎么做呢?回到我简单的基线实现,我能做的第一件事就是添加这个并行调用,也就是 Java Streams API 的这一部分。



现在它将并行处理这个管道,或者说这个流管道的一部分。只需添加这个单一方法调用,就可以将时间缩短到 71 秒,非常轻松的胜利。

 

如果你仔细想想,是的,它让我们的速度加快了不少,但并没有达到八倍的水平。可我们有 8 个 CPU 核心,为什么它没有八倍的速度?因为这个并行运算符适用于处理逻辑。所有这些聚合和分组逻辑都是并行发生的,但从内存中读取文件仍然是按顺序发生的。读取部分是按顺序进行,其他 CPU 核心依旧处于空闲状态,所以我们也想将其并行化。

 

新的 Java 版本都带有新的 API、JEP、Java 增强提案。其中之一是最近添加的外部函数和内存 API。


本质上它是一个 Java API,允许你使用原生方法。它比旧的 JNI API 更易用,还允许你使用本机内存。你可以管理自己的内存部分(如堆外内存),而不是由 JVM 管理的堆,并且你将负责维护它、释放它,等等。我们想在这里使用它,因为我们可以内存映射这个文件,然后在那里并行处理它。



首先我们确定并行度。我们的例子中是八个核心,这就是我们的并行度。接下来我们要对这个文件进行内存映射。在早期的 Java 版本中,你也可以使用内存映射文件,但你有大小之类的限制,你无法一次对整个 13 GB 的文件进行内存映射。而现在有了新的外部内存 API,我们就可以做到这一点。你映射文件。我们有这个 Arena 对象。这本质上是我们对这个内存的表示。有不同类型的 Arena,这里我只是使用这个全局 Arena,它可以从我的应用程序中的任何位置访问。现在我可以使用多个线程并行访问整个内存部分。

 

为了做到这一点,我们需要分割文件和内存表示。首先,我们将其大致分成八个相等的块。我们将整个大小除以八。当然,很有可能我们会分割到某一行的中间,而理想情况下,我们希望我们的工作进程能够处理整行。这里发生的事情是,我们转到了文件的大约八分之一,然后继续转到下一个行结束符。那么这里就是这个块的结尾,也是下一个块的起点。然后我们处理这些块,我们启动线程,然后将它们连接起来。现在我们真正在整个周期内都利用了所有 8 个内核,进行 I/O 时也一样。

 

有一个警告。从本质上讲,其中一个 CPU 核心总是最慢的。在某个时候,其他七个核心都会等待最后一个核心完成,因为数据的分布有点不均匀。人们最终的做法是不再使用 8 个块,而是将这个文件分成更小的块。本质上,他们积压了这些块。每当其中一个工作线程处理完当前块时,它就会去处理下一个块。通过这种方式,你可以确保所有 8 个线程始终得到平等利用。事实证明,理想的块大小是 2 兆字节。为什么是 2 兆字节?我使用的这台机器上的这个 CPU 有 16 兆字节的二级缓存,8 个线程每次处理 2 兆字节。这个数值在预测 I/O 等方面是最好的。这也表明,我们确实深入到了具体的 CPU 和架构的层面来真正优化该问题。

解析

我们更深入地分析一下。我们已经了解了如何利用多个 CPU 核心,但具体处理每一行时究竟发生了什么?我们仔细看看。

 

我们想摆脱最初的使用正则表达式等分割文件的做法,那样效率并不高。我能想到的办法是,只需逐个字符地处理这些输入行即可。


这里差不多是一个状态机。我们读取字符,继续读取行,直到没有字符。然后我们使用将站点名称与温度值分隔开的分号字符来切换这些状态。根据我们所处的状态,我们是读取组成站点名称的字节,还是读取组成测量值的字节?我们需要将它们添加到某个构建器或缓冲区中,以聚合这些值。

 

然后,如果我们在一行的末尾,也就是说我们找到了行结束符,那么我们需要使用建立的这两个缓冲区来记录站点和测量值。对于测量值,我们需要了解如何将其转换为整数值,这也是人们想出的办法。这个问题被描述为双精度或浮点运算,因此值是 21.7 度,但同样,我总是只遇到一个小数位。人们意识到,这个数据实际上总是只有一个小数位。我们可以利用这一点,只需将数字乘以 100 即可将其视为整数问题,作为计算方法。然后在最后,将其除以 100 或 10。这是人们经常做的事情,我低估了他们会在多大程度上利用该数据集的特定特性。



所以我们处理或使用这些值。如果我们看到减号就取反这个值。如果我们看到两位数字中的第一个,就把它乘以 100 或 10。这样做,我们可以把时间缩短到 20 秒,已经比最初的基线实现快了一个数量级。

 

到目前为止没有什么真正神奇的事情。你也应该得到一个启示,继续做这样的事情有多大意义?如果这是你在日常工作中面临的问题,也许就此打住吧。它可读性好,可维护性好。它比原生基线实现快了一个数量级,所以这相当不错。

 

当然,为了应对这一挑战,我们可能需要走得更远。我们还能做什么?我们可以再次回到并行的概念,尝试一次处理多个值,现在我们有了不同的并行方法。我们已经看到了如何充分利用所有 CPU 核心。这是并行度的一方面。我们还可以考虑扩展到多个计算节点,这通常是我们对大规模数据存储所做的事情。对于这个问题它并不那么重要,我们必须拆分该文件并将其分发到网络中。也许不是那么理想,但那将是另一个极端。而我们也可以朝另一个方向发展,在特定的 CPU 指令内作并行化。这就是 SIMD(单指令多数据)所做的事情。

 

基本上所有这些 CPU 都有扩展指令,允许你一次将相同类型的操作应用于多个值。例如,在这里我们想找到行尾字符。现在我们不再逐字节对比,而是可以使用这样的 SIMD 指令将其应用于 8 个或 16 个甚至更多字节,当然这会大大加快速度。问题是,在 Java 中,你没有很好的方法来利用这些 SIMD 指令,因为它是一种可移植的抽象语言,它不允许你降低到这种级别的 CPU 底层上。



好消息是我们可以用上面这个向量 API,它仍在孵化中,在第八个孵化版本左右。这个 API 现在允许你在扩展中使用这些向量化指令。你可以使用这个相等运算符进行类似这样的比较调用,然后它将被转换为底层架构的正确 SIMD 指令,转换为 Intel 或 AMD64 扩展。对于 Arm,它也会这样做。如果你的机器没有任何向量扩展,它将回退到标量执行。这是指令级别的并行化。我对此作了另一次演讲(https://speakerdeck.com/gunnarmorling/to-the-moon-and-beyond-with-java-17-apis),其中向你展示了如何使用 SIMD 解决 FizzBu​​zz。

 

这种模式可以一次将相同的操作应用于多个值,此外我们还可以执行所谓的 SWAR,即寄存器内的 SIMD。



这里的想法是,做同样的事情,比如一次处理多个值的相等操作,我们也可以在单个变量中执行此操作。如果你有 8 个字节,我们也可以看到一个 long,那是 64 位,那也是 8 个字节。我们可以将正确级别的位级魔法应用于 long 值,然后将此操作应用于所有 8 个字节。这里会有位级屏蔽和移位等等事情。Richard Startin 有一篇非常好的博客文章,一步一步地向你展示了如何做到这一点,或者如何使用它来查找字符串中的第一个零字节。



我把数学公式放在右边,你会看到,这实际上给了你一个长整型的第一个零字节。这就是寄存器内的 SIMD,SWAR。现在有趣的是,如果你看一下这段代码,会发现这里缺少了一些东西。有人意识到我们这里缺了什么吗?这段代码中没有 if、没有条件、没有分支。这实际上非常重要,因为我们需要记住 CPU 实际上是如何工作的。如果你看看 CPU 如何获取和执行我们的代码,会发现它总是采用这种流水线方法。每条指令都有这个阶段,它将从内存中获取,解码,执行,最后写回结果。现在这些事情中的多个操作是并行发生的。当我们解码一条指令时,CPU 已经去获取下一条指令了。这是一种流水线并行化方法。



当然,CPU 实际上需要知道下一条指令是什么,否则我们就不知道要获取什么。为了让它知道,我们实际上不能有任何 if,因为那样我们就不知道要往哪个方向走。如果你有一种用这种无分支方式表达这个问题的方法(就像我们之前看到的那样),那么这对 CPU 中分支预测器来说非常有益,这样它就总能知道下一条指令是什么。我们从来没有遇到过一种情况,就是实际上需要刷新这个管道,只因为我们在这个预测执行中走了一条错误的路径。



人们经常使用的资源之一是这本书《黑客的喜悦》。如果你对此感兴趣,我建议每个人都去买这本书。比如这个问题,比如在字符串中找到第一个零字节,所有这些算法、例程都在这本书中描述过了。如果这件事让你兴奋,一定要看看这本书买下来。

然后灾难发生了

有一天我醒来,突然间所有这些结果都不一样了。它比以前快了两倍。我运行了前一天运行过的一个实现,突然间速度快了很多。我在想发生了什么事?

 

其实这个负载最初是在虚拟服务器上跑的。我得到了专用的虚拟 CPU 核心,这样我就不会在那台机器上有任何吵闹的邻居了。我没想到的是他们会直接把这个负载转移到另一台机器上。我不知道为什么会这样。也许原因是随机的。也许他们看到了那台机器上有很多负载。无论如何,它只是被转移到了另一台比以前更快的主机上。这当然对我来说是一个大问题,因为到目前为止我所做的所有对比都是错误的,不再具有可比性。我意识到自己需要一台专用服务器,正如我提到的,我的雇主 Decodable 挺身而出赞助了它。

 

当然我还需要维护方面的帮助,因为我不是运维方面的专家。例如,你可能想关闭超线程,或者你想关闭核心加速功能以获得稳定的结果。我并不擅长做这些事情,但社区的 René 来帮忙了。他主动提出帮助设置这个东西。有很多这样的人都来帮忙,然后人们实际上构建了一个 TCK,一个测试套件。


它实际上是一个你必须通过的测试套件。你不仅想要快,还想要正确。人们构建了这个测试套件,它实际上随着时间的推移而增长。然后,每当有新的提交、新的条目进来时,它首先必须通过这些测试。然后如果它有效,那么我会去评估它并运行实现。这就是它的样子。



它有带有测量值的示例文件和一个预期文件,然后测试运行器将针对该组文件处理实现,并确保结果正确。另一件事也非常重要,我必须在那台机器上运行所有这些程序。有很多事情与此相关,例如确保机器配置正确。对所有程序我都会运行五次,丢弃最快和最慢的结果。

簿记(Bookkeeping)

然我们再谈一件事,也是非常重要的,就是簿记。回到我展示的初始代码,里面有一个 Java Streams 实现,我使用这个收集器将值分组到不同的存储桶中,每个气象站名称都是如此。人们意识到,这里也是可以做大量优化的。凭直觉,你会为此使用 HashMap。你将使用气象站名称作为该 HashMap 中的键。Java HashMap 是一种通用结构,适用于一系列用例。然后,如果我们想为一个特定用例获得最佳性能,那么我们最好自己实现一个定制的数据结构。


这就是我们在这里看到的,它比较简单。这里我们想跟踪每个气象站名称的测量值。它就像一张地图,由一个数组支持。

 

现在的想法是,我们获取车站名称的哈希键,并将其用作该数组中的索引,在数组中的特定位置,我们将管理特定车站名称的聚合器对象。我们获取哈希码,并希望不会溢出。这就是为什么我们将其与数组大小的逻辑结尾一起获取。我们始终在数组中对其进行良好的索引。然后我们需要检查,在数组中的特定位置是否已经存在某些内容?如果没有,则意味着我们手中有特定车站的第一个实例,因此是汉堡市的第一个值或慕尼黑的第一个值。我们只需在那里创建这个聚合器对象并将其存储在数组中的特定偏移量处。当然另一种情况是,我们转到数组中的特定索引,并且那里很可能已经存在某些内容。如果你有同一车站的另一个值,则那里已经存在某些内容。

 

问题是我们还不知道,这实际上是我们手中的特定键的聚合器对象,还是其他东西?因为多个站名可能具有相同的键。这意味着,在这种情况下,如果某个东西已经存在于这个特定的数组槽中,则需要回退并对比实际名称。只有当传入的名称也是该槽中聚合对象的名称时,我们才能将值添加到该名称中。这就是为什么它被称为线性探测。否则,我们将继续在该数组中迭代,直到我们找到一个空闲的槽,然后我们可以在那里安装它,或者我们找到了我们手中的键的槽。对于这个案例,它的性能实际上比我们仅使用 Java HashMap 所能获得的性能要好得多。

 

当然,这在很大程度上取决于我们用来查找该索引的特定哈希函数。这可以追溯到人们真正针对特定数据集进行了大量优化的工作上,他们使用了对特定数据集无冲突的哈希函数。这有点违背我的想法,因为问题在于这个文件,正如我提到的,它的大小为 13 GB,而我没有很好的方法将 13 GB 分发给大家,所以他们必须自己生成它。我有数据生成器,每个人都可以使用这个生成器为自己创建文件,然后将其用于自己的测试目的。问题是在这个数据生成器中,我有一个特定的密钥集。我有大约 400 个不同的车站名称,这只是一个例子,但人们非常字面地理解了它,然后他们针对这 400 个车站名称进行了大量优化。他们对这 400 个名称使用了不会发生任何冲突的哈希函数。人们会利用他们能利用的一切。

 

所有这些的问题在于它也给我带来了很多工作,因为你无法真正证明没有哈希冲突。实际上,每当有人提交他们的实现时,我都必须去检查他们是否真的处理过这种情况,即两个站会创建相同的密钥,他们是否相应地处理了这些冲突?因为如果你不这样做,就会回到缓慢的时代,你会非常快,但你会不正确,因为你没有正确处理所有可能的名称。这是我为自己设置的一个小陷阱,这意味着我总是必须检查这一点,并在拉取请求模板中询问人们,如果你有一个自定义映射实现,你在哪里处理冲突?

GraalVM:JIT 和 AOT 编译器

上面提到了三件大事,并行化,然后是使用 SIMD 和 SWAR 处理所有这些解析,以及用于簿记的自定义哈希映射。然后还有更具体的技巧,我会提到其中的几个,想给你一些现成的灵感。



现有的一个工具是 Epsilon 垃圾收集器,这是一个非常有趣的垃圾收集器,因为它不收集任何垃圾。这是一个无操作实现。如果你有常规的 Java 应用程序,它不会是一个好主意。因为你会不断分配对象,如果你不执行任何 GC,就将在某个时候耗尽堆空间。在这个挑战中人们意识到,我们实际上可以以一种在处理循环中不进行任何分配的方式来实现这一点。我们最初在引导程序时会做一些分配,但稍后不会再创建任何对象。我们只有可以重用的数组,就像可变结构一样,我们可以更新它们。

 

然后我们就不需要任何垃圾收集了,也不需要任何 CPU 周期花在垃圾收集上,这意味着我们可以更快一点。我认为这是一件有趣的事情。另一件事,你可以看到这里人们使用了很多 GraalVM。GraalVM 实际上有两个作用。它是一个提前编译器,因此它会获取你的 Java 程序并从中发出原生二进制文件。这有两个优点。首先它占用更少的内存。其次它启动速度非常快,因为它不必进行类加载和编译等所有操作,这一切都发生在构建时。如果你有这个原生二进制文件,它启动速度很快。

 

一开始我认为在启动时节省几百毫秒对处理 13 GB 的文件不会有什么影响,但实际上它确实有影响。AOT 编译器和大多数最快的实现实际上都使用了带有 GraalVM 的 AOT 编译器。也可以使用它来替代 JVM 中的即时编译器。你可以将其用作 C2 编译器的替代品。我并不是说你应该总是这样做。这有点取决于你的特定工作量和你做的事情。而这个问题非常适合这样做。人们只需使用 GraalVM 作为 JVM 中的 JIT 编译器,就可以获得大约 5% 的良好改进。我推荐你尝试一下,因为它基本上是免费的。你只需要确保你使用的 JVM 或 Java 发行版有 GraalVM 作为 C2 编译器的替代品。



还有其他一些东西,比如不安全。我发现上图右边的构造很有趣,如果你看一下,这是我们的内部处理循环。我们有一个 scanner 对象。我们尝试获取下一个值,尝试处理它们,等等。这里看到的是,我们在以顺序方式编写的程序中有三次相同的循环。你会说这三个循环是一个接一个地运行。实际发生的情况是,由于 CPU 有多个执行单元,编译器会发现这实际上可以并行化,因为这些循环之间没有数据依赖关系。我们可以采用这些循环并同时运行它们。我发现这很有趣。为什么是三次?因为是经验决定的。想出这个主意的 Thomas 网友尝试了两次、四次,结果发现三次循环是该机器上最快的。在其他机器上可能会有所不同。

结果

那么你一定很好奇,我们最后能跑多快?


这是我一开始的 8 个 CPU 核心的排行榜。当我转移到自己的本地机器时,试图保持相同的速度。利用了这 8 个核心后,我们缩短到了 1.5 秒。我没想到 Java 能跑得这么快,在 1.5 秒内就能处理 13 GB 的输入。我觉得这相当令人印象深刻。

 

情况会变得更好,因为我有了一台强大的服务器,有 32 个核心和 64 个线程,还有超线程。当然,我想看看我们能跑多快?然后我们缩短到了 300 毫秒。


对我来说这简直令人难以置信,超级令人印象深刻。此外,正如我所提到的,人们在其他语言、其他平台上也做到了这一点,而 Java 确实处于领先地位,所以在其他平台上你不会有太大的优势。

 

还有另一项评估,因为我提到我有一个包含 400 多个站名的数据生成器,人们通过选择特定的哈希函数等对其进行了大量优化。有些人意识到这实际上不是我的本意。我想看看这一般来说能有多快?于是我们有了另一个排行榜,其中实际上有 10k 个不同的站名。


正如你看到的,它实际上有点慢,因为你真的无法针对该数据集进行那么多优化。此外,这里排名靠前的人也不一样了。

漫长的旅程

人们为此工作了很长时间,挑战持续了整个 1 月。当人们想出某种技巧时,其他人也会很快将其采纳到自己的实现中。这是一段漫长的旅程。


上面是 Roy Van Rijn 的实现,他保存了非常好的日志,你可以看到他随着时间的推移是如何进步的。如果你往下看,你会看到他开始有点挣扎,因为他做了一些改变,实际上它们比以前慢了。问题是他跑在了他的 Arm MacBook 上,这显然与我运行它的机器有不同的 CPU 和不同的特性。他看到了一些本地改进,但实际上在评估机器上速度更快。你可以在底部看到,他尝试购买一台 Intel MacBook,以便有更好的机会在本地执行某些操作,并且该操作在那台机器上的性能也会更好。我发现 Java 中也发生了这种情况,这真的很令人惊讶。这个层面如此深入,特定的 CPU 甚至它具体是哪一代都会在这里产生影响。

你应该这样做吗?

你应该做这些优化工作吗?这要视情况而定。如果你在开发企业应用程序,我知道你大多数时候都在处理数据库 I/O。达到那个级别并试图避免业务代码中的 CPU 周期可能会很浪费时间。而如果你要应对这样的挑战,那么这可能是一件有趣的事情。我建议的是,例如某个实现比我的基线快一个数量级。它仍然非常易读,你在这方面没有任何陷阱。这种优化就很不错,你应该非常清楚你是否想这样做。

 

或者如果你想加入 GraalVM 团队,也可以尝试一下极限优化。我前几天才知道,在比赛中一位名叫 Mary Kitty 的选手被 Oracle 的 GraalVM 编译器团队聘用了。

总结

这场挑战不仅影响了 Java 社区,还影响了其他生态系统。在 Snowflake 中,他们发起了一项“一万亿行挑战”。


GitHub 存储库中有这个挑战的展示和说明。你可以去那里看看 Rust 和 OCaml 中的所有实现,以及我从未听说过的所有好东西,看看他们以非常友好、有竞争力的方式做了些什么事情。


上面是一些统计数据。

 

从经验教训来看,如果我想再来一次,我必须在规则方面真正规范化,实现更多自动化,并与社区合作。Java 语言慢吗?我认为我们已经揭穿了这一谎言。明年我会再做一次吗?我们拭目以待。

 

原文链接:https://www.infoq.com/presentations/1brc/#idp_register/

2025-03-13 18:438068

评论

发布
暂无评论

ToB产品如何自传播(下)

石云升

产品经理 产品思维 10月月更

IM系统消息丢失问题排查反思

轻口味

IM Android; 10月月更

存量时代会员深度运营逻辑

boshi

深度思考 运营

Leetcode 题目解析:279. 完全平方数

程序员架构进阶

算法 LeetCode 动态规划 10月月更

在线下划线转驼峰,驼峰转下划线工具

入门小站

工具

Alibaba最新微服务持续集成,内含(Jenkins+Docker+Spring Cloud+K8S)

Java 架构 面试 程序人生 编程语言

阿里架构师总结Go语言和java语言之间的对比联系

hanaper

隐蔽的角落-这次我们只聊Cilium IPAM

Lance

面试作弊神器?!阿里P8亲自撰写的这份Java最新面试手册

Java 程序员 架构 面试 后端

linux中vi,vim操作技巧

入门小站

Linux

Groovy记录(1)-GroovyClassLoader

春秋易简

Groovy

Groovy 记录(2)-CompilationUnit

春秋易简

拿蚂蚁offer,全靠阿里P8大牛总结的Java架构开发手册

Java 编程 程序员 架构 面试

【Vuex 源码学习】第十二篇 - Vuex 插件机制的实现

Brave

源码 vuex 10月月更

并发相关的性质学习笔记

风翱

并发 10月月更

Prometheus 基础查询(一)

耳东@Erdong

Prometheus 10月月更

粪菌移植的背后,肠道菌那些你不知道的事儿

脑极体

Serverless 工程实践 | 零基础上手 Knative 应用

阿里巴巴云原生

阿里云 Serverless 云原生 Knative

gRPC,爆赞

AlwaysBeta

golang 编程 gRPC 后端 Go 语言

Node.js 日志之 winston 实践

devpoint

nodejs winston logger 10月月更

SSRF漏洞实例分析

网络安全学海

网络安全 信息安全 渗透测试 WEB安全 漏洞分析

面试官:你说说ThreadLocal为什么会导致内存泄漏?

长河

Java

前后端、多语言、跨云部署,全链路追踪到底有多难?

阿里巴巴云原生

阿里云 云原生 全链路追踪

「架构师教程」二十年架构师「马士兵」大牛的Java高级架构师教程

Java 编程 程序员 IT 计算机

趣说Node.js的回调函数

Regan Yue

node.js JavaScrip Regan Yue 10月月更

业界良心啊!第五次更新的Spring Cloud Alibaba升级太多内容

Java 编程 程序员 IT 计算机

SpringBoot 实战:在 RequestBody 中优雅的使用枚举参数(原理篇)

看山

Java Spring Boot Effective Spring 10月月更

CSS架构之Components层

Augus

CSS 10月月更

双非学历为进大厂天天刷Java面试题,面试却履败,原因竟是算法?

Java 编程 程序员 架构 IT

实践篇 -- Redis客户端缓存在SpringBoot应用的探究

binecy

缓存 springboot redis sentinel

Facebook宕机事故,暴露了上云不是唯一的答案

脑极体

Java 社区的一次十亿行数据编程挑战_编程语言_Gunnar Morling_InfoQ精选文章