多年来,Java 一直是编程世界里绕不开的老面孔。从 90 年代末的企业级应用到如今的大型服务端系统,Java 凭借“写一次,到处跑”的理念,占据了无数开发者的日常。它稳定、成熟、生态庞大,但也常被人诟病——臃肿、语法冗长、速度不够快。 于是,Java 的性能一直是开发者茶余饭后的讨论话题:它真的慢吗?在现代硬件和新特性的加持下,老牌语言还能跑出惊人的速度吗?本文作者 David Gerrells 用 Java 构建了一个 2000 万粒子的二维粒子模拟,挑战 SIMD、多线程和实时渲染的极限。通过这个例子,我们不仅能看看 Java 在性能上的表现,也能重新认识它作为一门老牌语言的潜力和魅力。 来源:https://dgerrells.com/blog/how-fast-is-java-teaching-an-old-dog-new-tricks 作者 | David Gerrells 责编 | 苏宓 出品 | CSDN(ID:CSDNnews) Java,这门让人又爱又恨的语言。 很多人提到它的第一反应是:老旧、笨重、臃肿,只是因为某些大企业在上世纪 90 年代被炒作忽悠了,才不得不用它。长久以来,它都背着“企业老古董”的名号。 可现在还真是这样吗? Java 是否已经僵化在那套老掉牙的面向对象思维中,注定被时代淘汰?还是说,这条“老狗”其实已经学会了一些新把戏? 这次的挑战就是:用尽 Java 的所有新特性,只靠 CPU,模拟尽可能多的粒子。看看能不能——干翻 Rust? 如果你懒得看完整篇,可以直接下载运行包(jar 文件,https://github.com/dgerrells/how-fast-is-it/blob/main/java-land/ParticleSim.jar)。代码也放在 GitHub 上(https://github.com/dgerrells/how-fast-is-it/tree/main/java-land)。你只需要在运行时要加上以下命令,启用 Java 的 Vector API(一个用于高性能计算的新功能)就行: java --add-modules jdk.incubator.vector --enable-preview -jar ParticleSim.jar
不妨一试! 开场热身 Java 对我来说有点特殊。它是我第一个真正深入学习的编程语言。主要原因,是因为当年我上大学时,学校几乎所有课程都用它作为标准语言。那已经是十多年前的事了。唉,时间真是残酷。
那时候我用 Java 做游戏开发,写过人生第一个多线程的粒子模拟器,还做过一个“致敬”Minecraft 的小游戏(虽然我本人从没玩过 Minecraft)。挺好玩的,当然按今天的标准看那代码相当“痛苦”,但当时的确很有成就感。 然后十年过去,我再一次翻出 Java,却看到一个“SIMD 实验性 API”的新闻。 我当时的反应是:“什么?Java 有 SIMD?太阳从西边出来了?” 没错,太阳真的从西边出来了。Java 现在真的支持 SIMD(Single Instruction Multiple Data,单指令多数据)了,还提供了一个抽象层,让你不用直接面对底层硬件那堆复杂指令。 对于初学者而言,如果你不清楚 SIMD 是啥,可以去搜一下,或者直接问个 AI,都行。 如果你懒得查,我简单说下:SIMD 是一套 CPU 指令,用来让处理器一次同时处理多个数字,而不是一个个来。很多现代编译器其实能自动帮你做这个优化,但不是每次都能做到完美。 麻烦在于:不同 CPU 的 SIMD 指令集各不相同。有的架构只能处理 128 位数据(比如一次 4 个 32 位浮点数),有的能到 256 位,甚至 512 位。这意味着如果你要手动写 SIMD 代码,往往得针对每种 CPU 写一份,或者用别人封装好的库——不过那样就少了点“折腾”的乐趣。 而 Java 的特别之处就在这里:它号称“写一次,到处跑”。 那它的 SIMD API 真的能跨平台、跑得又快又稳吗?这就得试试看了。好在我最近刚好用 Rust 和 Swift 写过多线程的 SIMD 粒子模拟器。以前也用 JavaScript 和 Go 写过类似的,但那俩语言本身就比较慢,也没有原生的 SIMD 支持(除非你仔细研究 V8 引擎那一套)。 另外,Java 还有挺多新的“花活”,比如 Lambda 表达式和多线程迭代器——理论上可以让并行编程变得非常轻松。挺棒的。 等会我会具体看看那些 API,但在那之前,我得先搞清楚一件事: 怎么在 Java 里把像素绘制到屏幕上。 在 Java 里绘制 我想保持和之前在 Rust、Swift 做的实验差不多的结构: 写一个小型二维粒子模拟,屏幕上有一个“重力点”,能吸引周围的粒子。每个粒子就是一个像素点。整个过程尽量只用 CPU,GPU 不参与。 那问题来了:在 Java 里,我要怎么在窗口上画像素? 之前在 Rust 里,我用的是一个窗口库,它帮我处理不同操作系统的窗口细节。Swift 则是“纯正苹果血统”,自带一套完善的图形 API,拿来就能用。而 Java 呢?它确实也提供了一组跨平台的 UI API,可以在所有系统上跑。虽然界面看起来不太“原生”,但至少能正常工作。 我上次写 Java UI 的时候,主流还是内置的 Swing库。那时人们说它要被JavaFX替代。 结果现在一看……JavaFX “貌似” 成了标准,但你得手动去下载 jar 包,然后通过 Maven 或 Gradle 引入。 所以算了。我还是用 Swing 吧——因为我实在不想折腾 Maven 和 Gradle。 还是想吐槽一下:Java 到了 2025 年了还没有默认包管理器?我就不能像其他语言一样 jfaster add jfx 一键搞定?真让人失望。 当然,这事也没那么严重。因为就算用上 JavaFX,你要在窗口里画像素,最终还是得靠一个叫 BufferedImage的类。顾名思义,BufferedImage 就是一块存放在内存里的像素缓冲区,你可以在上面“画”,再把它显示到屏幕上。 我就不贴 Swing 的那些啰嗦样板代码了。核心思路是:你创建一个 JFrame(窗口),往里面塞各种 UI 组件。然后写一个自定义的Panel 类(这里用来显示粒子模拟),继承或实现 Swing 的接口,把自己的绘制逻辑加进去。典型的面向对象玩法。 顺带一提,现在 Java 其实有个更“现代”的 main 函数写法,可以不用写类。可惜它跟 Swing 不兼容。 public class ParticleSim { public static void main(String[] args) { new ParticleSim.createAndShowGUI; } private void createAndShowGUI { JFrame frame = new JFrame("Sips Java"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); int width = 1200; int height = 800; ParticlePanel particlePanel = new ParticlePanel(width, height); frame.add(particlePanel); frame.pack; // resize child components frame.setLocationRelativeTo(null); // center frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // won't actual close without this frame.setVisible(true); // ya i know, default is false particlePanel.startSimulation; }}
ParticlePanel 这个类很长,很啰嗦,很典型的 Java 风格。 一开始,我把逻辑放在事件分发线程上,通过重写 JPanel 的 paint 方法来画画,然后用一个 Timer 定时调用 tick 方法来触发粒子模拟。大概结构就是这样。 ⚠️ 提醒一下,这段代码比较长,所以才说“很 Java 风格”。 // importspublic class ParticleSim { public static void main(String[] args) { new ParticleSim.createAndShowGUI; } private void createAndShowGUI { JFrame frame = new JFrame("Vector API Particle Sim (Requires flags)"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); int width = 1600; int height = 900; ParticlePanel particlePanel = new ParticlePanel(width, height); frame.add(particlePanel); frame.pack; frame.setLocationRelativeTo(null); frame.setVisible(true); particlePanel.startSimulation; }}class ParticlePanel extends JPanel implements ActionListener, MouseListener, MouseMotionListener { private static final int NUM_PARTICLES = 80_000_000; private static final int UPDATE_RATE = 1000 / 60; private float positionsX = new float[NUM_PARTICLES]; private float positionsY = new float[NUM_PARTICLES]; private float velocitiesX = new float[NUM_PARTICLES]; private float velocitiesY = new float[NUM_PARTICLES]; private final BufferedImage image; private final byte pixelArray; private final int panelWidth; private final int panelHeight;// other input variables public ParticlePanel(int width, int height) { // setup state image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); initializeParticles; // setup input and time tracking } private void initializeParticles { // init particles } public void startSimulation { timer.start; } @Override public void actionPerformed(ActionEvent e) { if (isTicking) { return; } isTicking = true; long now = System.nanoTime; float deltaTime = (now - lastTickTime) / 1_000_000_000.0f; lastTickTime = now; updatePhysics(deltaTime); renderToPixelArray; repaint; frames++; isTicking = false; } private void updatePhysics(float deltaTime) {// update logic } private void renderToPixelArray { // render } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); g.drawImage(image, 0, 0, this); } // input overloads}
大部分都是标准的 Java Swing 代码。渲染部分稍微有点意思:我把每个粒子映射到一块扁平的像素缓冲区(flat buffer)里,然后再显示出来。 private void renderToPixelArray { final int w = panelWidth; final int h = panelHeight; final byte empty = 0; Arrays.fill(pixelArray, empty); for (int i = 0; i int px = (int) positionsX[i]; int py = (int) positionsY[i]; int index = py * w + px; // stay in bounds. if (px 0 || px >= w || py 0 || py >= h) { continue; } int lu = pixelArray[index] & 0xFF; lu = Math.min(255, lu + 1); pixelArray[index] = (byte) lu; }}
真正有意思的部分是 updatePhysics方法——这里才是“魔法发生的地方”。 不同 CPU,SIMD 通道大小不同 要记住,SIMD 在不同硬件上支持的“通道大小”(lane size)不同。为了效率,最好用支持的最大通道大小。Java 的 SIMD API 通过定义“species”(种类)来处理这个问题。每个 species 对应一种数据类型,比如 float 或 int,然后你就可以根据这个 species 的大小,把数据加载到 Vector 类型中,或者从 Vector 中取出数据。 大概就是这样工作的: private static final VectorSpeciesprivate static final int LANE_SIZE = F_SPECIES.length;private static final FloatVector PULL_VEC = FloatVector.broadcast(F_SPECIES, 500f);
这就让代码可以比较通用地适配不同硬件。通常推荐使用preferred,据说性能最好。还有一个MAX,会用最大的通道大小,但不一定性能最好。在我的 M1 上,preferred 和 MAX 都输出 4 个浮点数(128 位)。我就用 preferred 就好。 顺便说一句,我不知道为什么,但我特别喜欢这里叫 species这个名字,真让人忍俊不禁。我能想象一大群人开会讨论,最后才定下这个名字的场景。 多线程 SIMD 我一不小心就想跳过单线程,直接用 Java 的 stream API弄多线程 SIMD 了。思路是这样的:用 IntStream.range 把粒子按照最大通道数切块,然后对这些块进行并行迭代,实现多线程计算。 final int w = this.panelWidth;final int h = this.panelHeight;final float wFloat = (float) w;final float hFloat = (float) h;final FloatVector DT_VEC = FloatVector.broadcast(F_SPECIES, deltaTime);// var works toofinal var MOUSE_X_VEC = FloatVector.broadcast(F_SPECIES, (float) mousePosition.x);final var MOUSE_Y_VEC = FloatVector.broadcast(F_SPECIES, (float) mousePosition.y);// more vectorsfinal int VECTOR_CHUNKS = NUM_PARTICLES / LANE_SIZE;final int SCALAR_START_INDEX = VECTOR_CHUNKS * LANE_SIZE;IntStream.range(0, VECTOR_CHUNKS).parallel.forEach(chunkIndex -> { int i = chunkIndex * LANE_SIZE; var px = FloatVector.fromArray(F_SPECIES, positionsX, i); var py = FloatVector.fromArray(F_SPECIES, positionsY, i); var vx = FloatVector.fromArray(F_SPECIES, velocitiesX, i); var vy = FloatVector.fromArray(F_SPECIES, velocitiesY, i); if (mouseIsPressed) { var dx = MOUSE_X_VEC.sub(px); var dy = MOUSE_Y_VEC.sub(py); var distSq = dx.mul(dx).add(dy.mul(dy)); var gravityMask = distSq.compare(GT, MIN_DIST_SQ_VEC); if (gravityMask.anyTrue) { var dist = distSq.sqrt; var forceX = dx.div(dist).mul(PULL_SCALED_VEC); var forceY = dy.div(dist).mul(PULL_SCALED_VEC); vx = vx.add(forceX, gravityMask); vy = vy.add(forceY, gravityMask); } } vx = vx.mul(FRICTION_DT_VEC); vy = vy.mul(FRICTION_DT_VEC); px = px.add(vx.mul(DT_VEC)); py = py.add(vy.mul(DT_VEC)); var maskLeftX = px.compare(LT, ZERO_VEC); var maskRightX = px.compare(GT, W_VEC); var maskBounceX = maskLeftX.or(maskRightX); vx = vx.blend(vx.mul(BOUNCE_MULTIPLIER_VEC), maskBounceX); px = px.blend(ZERO_VEC, maskLeftX); px = px.blend(W_VEC, maskRightX); var maskTopY = py.compare(LT, ZERO_VEC); var maskBottomY = py.compare(GT, H_VEC); var maskBounceY = maskTopY.or(maskBottomY); vy = vy.blend(vy.mul(BOUNCE_MULTIPLIER_VEC), maskBounceY); py = py.blend(ZERO_VEC, maskTopY); py = py.blend(H_VEC, maskBottomY); px.intoArray(positionsX, i); py.intoArray(positionsY, i); vx.intoArray(velocitiesX, i); vy.intoArray(velocitiesY, i);});
多线程的使用方式和 Rust、Swift 非常相似,更重要的是,它真的能跑起来。Java 的 var 关键字让代码看起来更简洁一些,但我这种老手通常还是喜欢写显式类型。这里有个问题,细心的人可能已经注意到了:粒子数量不一定能被 SIMD 的通道数整除。这意味着会有剩余的粒子暂时没被处理。 处理方法有两种: 继续用向量方式处理,给空余通道填充占位; 用一个非向量化的循环单独处理剩下的粒子。 我选了第二种方式,但为了简洁,这里就不展示代码了。 效果是可以的,这里展示 2000 万粒子的模拟效果。 速度不快,大概只能跑到 20 帧每秒,而且看起来也不太吸引人。我想我知道原因了。 大改造 慢的原因之一不是模拟本身,而是渲染。目前代码是随机访问像素缓冲区的,这样效率很低。在 Rust 和 Swift 的版本里,大部分时间也是耗在这里,Java 也不例外。 除了把这些操作分摊到更多线程之外,几乎没什么优化空间。多线程能提速,但不能解决缓存访问冲突的问题。 还有一些其他问题。首先,渲染循环现在运行在事件分发线程上,这必须改掉。虽然改了之后输入处理会更复杂。整体效果看起来也比较单调、无聊。 在 Rust 和 Swift 版本里,我是通过按粒子相对于窗口的 x/y 距离缩放 RGB 颜色来给像素上色的。 这次我想换个玩法:给每个粒子分配一个独立颜色,之后再加点花哨的效果,同时支持窗口缩放、平移,还能让粒子速度变慢,提升使用体验。 我会跳过中间的琐碎步骤,直接说最终方案。 第一步:脱离事件分发线程 我打算用 Java 的新 lambda 特性,把一个函数传给线程对象来执行。 public void startSimulation { if (!running) { running = true; gameLoopThread = new Thread(this::gameLoop); gameLoopThread.start; }}
游戏循环会用一个“忙等待”的 while 循环——每次循环会暂停一小段时间,然后再检查是否可以渲染下一帧。我希望在这小段暂停时间里,从事件分发线程轮询输入,而不是等到下一帧渲染时才处理。这样可以让模拟更加响应及时。不过,有些事件仍然需要按照渲染帧率来处理,比如平移,因为这类操作需要考虑时间差来计算移动距离。 private void gameLoop { lastTickTime = System.nanoTime; while (running) { long now = System.nanoTime; long timeElapsed = now - lastTickTime; this.processInputRequests; if (timeElapsed >= NS_PER_TICK) { float deltaTime = timeElapsed / (float) NS_PER_SECOND; lastTickTime = now;// little gross this for (var key : this.keysPressed) { float speed = 500; if (this.velInputMap.containsKey(key)) { this.panDeltaInput.x += velInputMap.get(key).x * speed * deltaTime; this.panDeltaInput.y += velInputMap.get(key).y * speed * deltaTime; } } long tickStart = System.nanoTime; tick(deltaTime); long tickEnd = System.nanoTime; long tickDuration = (tickEnd - tickStart); long renderStart = System.nanoTime; render; Graphics2D g = (Graphics2D) getGraphics; g.drawImage(image, 0, 0, this); g.dispose; Toolkit.getDefaultToolkit.sync; frames++; long renderEnd = System.nanoTime; long renderDuration = (renderEnd - renderStart); // log frame time info } else { try { Thread.sleep(1); } catch (InterruptedException e) { // handle error } } }}
关键的一行是: if (timeElapsed >= NS_PER_TICK)
它用来判断是否需要渲染下一帧。 这样做还可以让渲染速度超过 60 帧/秒——对于那些性能超强的电脑或者粒子数量较少的情况,非常有用。 你可能会注意到,我现在是在更新像素后再画图。 我关闭了 Swing 自带的被动渲染(passive rendering),改为主动渲染。这样逻辑更简单,也更易控制。 接下来就是 tick 函数了。 private void tick(float deltaTime) { final int vectorizedEndIndex = (NUM_PARTICLES / LANE_SIZE) * LANE_SIZE; final int chunkSize = vectorizedEndIndex / CPU_COUNT; final var futures = new ArrayList // copy input data final int panDx = this.panDeltaInput.x; final int panDy = this.panDeltaInput.y; final float vScale = this.isSlowDownRequested ? this.inputVelScale : 0f; // reset for input thread, access is synced this.panDeltaInput.x = 0; this.panDeltaInput.y = 0; for (int i = 0; i int start = i * chunkSize; int end = (i == CPU_COUNT - 1) ? vectorizedEndIndex : start + chunkSize; ParticleUpdateTask task = tasks[i]; task.updateParams(i, start, end, this, deltaTime, panDx, panDy, vScale); futures.add(executorService.submit(task)); } for (Future> future : futures) { try { future.get; } catch (InterruptedException | ExecutionException e) { e.printStackTrace; } }}
这里有几个值得注意的点: 首先,借鉴 Rust 的做法,为了避免在多线程间频繁同步或加锁变量,我在进入核心模拟循环前,会先拷贝一份最新的输入数据。 大多数输入值会在主线程和事件分发线程之间同步。 这种加锁在非核心路径(非 hot path)下没问题,但对于粒子更新这种“热路径”,拷贝数据是必须的,否则性能会受很大影响。 另外,我对程序做了性能分析,发现大约 20% 的时间花在了 Java 的线程管理内部。这其实有些预料之中: 在 Swing、Rust 和 Go 中,使用那些花哨的线程迭代器会带来额外开销,既有内存压力,也有处理时间消耗。 相比之下,创建一个工作线程池并重复利用线程通常更快。 这也是一个机会,可以用 Java 的 Future 对象做一点异步控制。这里我没用太多,但确实挺有趣的。 ParticleUpdateTask 的代码大体没变,只是做了一些小调整。下面是几个关键部分。 class ParticleUpdateTask implements Runnable { // boiler plate variables, constructor, and param update function @Override public void run { // many local variables for (int i = startIndex; i FloatVector px = FloatVector.fromArray(F_SPECIES, positionsX, i); FloatVector py = FloatVector.fromArray(F_SPECIES, positionsY, i); FloatVector vx = FloatVector.fromArray(F_SPECIES, velocitiesX, i); FloatVector vy = FloatVector.fromArray(F_SPECIES, velocitiesY, i); if (mouseIsPressed) { FloatVector dx = MOUSE_X_VEC.sub(px); FloatVector dy = MOUSE_Y_VEC.sub(py); FloatVector distSq = dx.mul(dx).add(dy.mul(dy)); var gravityMask = distSq.compare(GT, minPullDist); if (gravityMask.anyTrue) { FloatVector dist = distSq.sqrt; FloatVector forceX = dx.div(dist).mul(gf); FloatVector forceY = dy.div(dist).mul(gf); vx = vx.add(forceX, gravityMask); vy = vy.add(forceY, gravityMask); } } px = px.add(vx.mul(deltaTime)).add(ox); py = py.add(vy.mul(deltaTime)).add(oy); vx = vx.mul(FRICTION_DT_VEC); vy = vy.mul(FRICTION_DT_VEC); px.intoArray(positionsX, i); py.intoArray(positionsY, i); vx.intoArray(velocitiesX, i); vy.intoArray(velocitiesY, i); } // non vectorized version var pixels = panel.threadPixelBuffers[id]; Arrays.fill(pixels, 0); for (int i = startIndex; i int px = (int) Math.min(Math.max(positionsX[i], 0), w - 1); int py = (int) Math.min(Math.max(positionsY[i], 0), h - 1); int index = py * w + px; pixels[index] = colors[i]; } }}
我去掉了粒子的边缘反弹逻辑,并进一步整理了 SIMD 代码。最重要的是,我给每个线程分配了一个本地像素缓冲区来绘制粒子。 我尝试过用 SIMD 优化像素缓冲区的截断(clamping)和索引计算,但结果反而更慢。还试过用 Java API 中一些更高级的 SIMD 函数,比如 fma,结果同样不如预期。 最后一个重大改动是:把每个工作线程的本地像素缓冲区直接累加到 BufferedImage 的数据里,这样合并更高效。 private void render { int buff = ((DataBufferInt) image.getRaster.getDataBuffer).getData; Arrays.fill(buff, 0); final int PIXEL_COUNT = buff.length; IntStream.range(0, CPU_COUNT).parallel.forEach(chunkIndex -> { int chunkSize = PIXEL_COUNT / CPU_COUNT; int start = chunkIndex * chunkSize; int end = (chunkIndex == CPU_COUNT - 1) ? PIXEL_COUNT : start + chunkSize; for (int i = start; i int color = 0; for (int localIndex = 0; localIndex int col = threadPixelBuffers[localIndex][i]; if (col != 0) { color = col; break; } } buff[i] = (0xFF 24) | color; } });}
我对线程本地像素缓冲区采用了“最后写入者胜出”的策略,而在合并时则是“第一次写入者胜出”。虽然让所有线程直接写同一个像素缓冲区也能工作,但会引入一些缓存一致性问题。给每个线程分配独立缓冲区虽然会增加内存占用,但性能更稳定。 说到内存:Java 的基线开销相当高,1 百万粒子大概需要 300ms。不过它的扩展表现和 Rust 基本一致,1 亿粒子的耗时和 Rust 差不多,再加上 300MB 的基线开销。 性能表现如何?先等等…… 像素的完美布局 我加了一个 colors 数组来存储粒子颜色,想让它填充出一些有趣的效果。一个朋友提议:根据粒子相对于中心点的角度上色,模拟一个色轮效果。我觉得这个主意挺酷的,但我想用OKLAB的方式来实现。 我对颜色格式了解不多,而且时间有限,所以让 AI 帮我写了段代码:根据粒子相对于屏幕中心点的角度,计算 OKLAB 色调。是不是完全正确?我怀疑。但我本想用现成库来做……你知道的,Gradle 的折腾。 我还增加了几种其他布局方式:一种按圆形排列(这段大部分代码是我写的)。另一种随机分布 N 个点,然后根据粒子到这些点的距离上色(这段我没写,但效果挺有意思) 好了,这里来个演示。 我觉得效果非常棒。你可以右键拖动来平移,或者用 WASD控制视角。空格键可以让粒子减速。我特别喜欢在这里玩耍。如果我把用来调试 OKLAB 上色的时间,拿来像玩这些交互那样投入,可能早就不用 AI 自己搞定了。但唉,我脑子里那个贪玩的小恶魔必须得到满足。 说到这个“贪玩的小恶魔”,我选择用 32 位整型表示粒子颜色,而不是用单字节或简单的开/关标志,是有原因的。 如果我根据用户定义的图片来给粒子上色呢?我可以根据粒子数量放大或缩小图片,还要注意把多余的粒子放在重复的位置。想想就很酷,对吧? 好吧,现在按数字 4,选择一张图片。 这里展示的是 2000 万粒子,用的是我最喜欢的一张图。 真是太酷了。它会根据粒子数量放大或缩小图片。这张图是 4K 分辨率,被放大到 2000 万粒子,几乎是原来的三倍大小。 看着效果,我都想加个缩放功能了,但我觉得该停手了。 那么,性能如何呢? Java 究竟有多快? 我会拿 Rust 来做对比,因为它目前表现最强。不过要注意,每个版本并不是完全一一对应的。Rust 每个像素只写 1 个字节,但相比于对像素缓冲区的随机访问,内存速度并不是渲染的瓶颈。Java 的 Vector API 仍处于实验性(incubator)阶段,未来可能会有所改进。 下面是 M1 Air 上的测试结果。 你看这个结果。Rust 在大多数规模下速度大约是 Java 的两倍。 两个版本都是在 tick 中填充像素缓冲区,这也是为什么渲染在不同粒子规模下变化不大。主要耗时是累加缓冲区并把它显示到屏幕,这个时间基本是固定的。 有意思的是,在 1 百万粒子时,Rust 似乎渲染时间更长,我这边多次测试都能复现,但原因我不清楚。 Rust 的内存分配速度快得多。这是因为 Java 都是在堆上分配内存。虽然可以用堆外内存(off-heap),创建速度通常快 2–3 倍,但我发现性能略微比堆上慢一些。差距不大,只有几个百分点,而且主要是在访问数据而非写入数据时的差距。 自从我上次用 Java 以来,它已经进步很多。没有借用检查的情况下,它的速度也只有 Rust 的一半左右! 可惜的是,虽然 Java 语言本身更友好、更易写,但整个生态系统仍有很多不足。 如果今天我要再写一款游戏,阻止我选择 Java 的,不是语言本身,而是搭建一个还算合理的构建系统、引入几个小库竟然这么麻烦。 我记得以前为了让所有 Vorbis 和 OpenGL 的 jar、dll 等都能正常工作,要写一个复杂的构建脚本,过程超级折腾,而这一点至今几乎没有改善。 我想,“老狗”是可以学新把戏的,但如果它还是在同一个灰尘满满、没有草地、也没有大黄球的老狗公园里混,它也没机会展示什么新本领。 不过,Java 永远在我心里占有一席之地。 智汇北纬,AI 新生态加速形成! 2025 百模论剑“西北旺杯”北京巡回赛暨 AI 论坛邀您共赴创新盛宴 立即预约,提前锁定直播间! 查看详情:https://www.toutiao.com/article/7564872836683219475 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |