Scala 3 让我们变慢了吗?
Scala 3 slowed us down?

原始链接: https://kmaliszewski9.github.io/scala/2025/12/07/scala3-slowdown.html

最近的一次Scala 2.13到3的迁移最初看起来是成功的——编译通过,测试全绿,初步部署也没有问题。然而,在生产环境中几个小时后,出现了一种神秘的性能下降,表现为Kafka延迟增加,尽管数据负载适中。 广泛的调查,包括负载测试和依赖回滚,都未能确定原因。性能分析显示CPU使用情况发生了显著变化:Scala 3版本在JIT编译器中花费了更多时间,并且令人惊讶的是,在Quicklens库中花费了更多时间。 Quicklens中的一个微妙的bug,导致Scala 3中低效的链式求值,被确定为罪魁祸首。升级该库解决了问题,恢复了与Scala 2.13的性能一致性。 关键经验是,看似无缝的迁移可能会隐藏性能回归,尤其是在使用依赖于元编程的库时。即使初步测试看起来成功,对性能热点进行彻底的基准测试也至关重要,以避免在生产环境中出现意外的瓶颈。

## Scala 3 性能问题与社区讨论 - 总结 一篇最近的博文详细描述了将 Scala 项目从 2.13 版本迁移到 3 后遇到的性能下降问题,具体与新的 `@inline` 关键字行为有关。在 Scala 2 中,`@inline` 是对编译器的建议,而在 Scala 3 中则强制执行,导致过度内联和 JIT 编译开销。作者发现了一个导致低效链式求值错误的 bug,最终通过库更新解决了。 Hacker News 的讨论强调了对 Scala 3 开发的更广泛担忧。许多人同意迁移具有破坏性,工具仍然落后。一些人批评语法更改(特别是可选的无大括号风格)是不必要的,并且会分散注意力。 一个反复出现的主题是,Scala 的开发优先考虑了学术理想,而不是实际的行业需求,这与 Kotlin 更务实的做法形成对比。人们对 Scala 的可维护性、支持以及“类型安全教会”表示担忧。虽然承认 Scala 的强大功能,但许多人认为 Kotlin 提供了一种更易于访问和良好支持的体验,从而导致其采用率不断提高。另一些人则为 Scala 辩护,指出其表达能力和 JVM 的性能优势。
相关文章

原文

Is this clickbait? Not really.
Is this the fault of the language or the compiler? Definitely not.
Rather, it was part of a rushed migration. Sharing the lessons learned in the process.

I was refreshing one of our services. Part of this process was to migrate codebase from Scala 2.13 to Scala 3. I’ve done this a few times before and overall had a positive experience. Well, at least until we talk about projects with macro wizardry.

The service in question had no macros at all, but it was at the heart of data ingestion, so performance was not an afterthought.

I did it as usual - updating dependencies, compiler options and some type/syntax changes.

Then after resolving few tricky implicit resolutions and config derivations, project compiled on Scala 3.7.3 🎉

All tests passed, end-to-end flow locally works perfectly fine, so I decided to roll out the changes in a testing environment. Similarly, no issues at all. No concerning logs, all metrics ranging from infrastructure, through JVM up to application level look healthy.

With that in mind, I began a staged rollout. Again, all seem good. I kept observing the service but it looked like my job is done.

Well, as you probably can guess, it wasn’t.

The mysterious slowdown

After 5-6 hours, Kafka lag started increasing on a few environments. Of course, this wasn’t something new. Most often it is caused by a spike of data. We have pretty advanced machinery to deal with that. Usually the lag resolves by itself without any manual action.

However, this time something was off. Upstream load turned out to be relatively modest, yet we needed much more instances of the service - meaning the processing rate per instance dropped. I was confused to say the least. Why would it decrease the processing rate just on these environments?

Anyway, we decided to rollback the changes - this brought the rate back.

Digging deeper

I came back to testing. In particular, load testing. However similarly as on production environments I did not notice regression. So I played around with different payloads and granularity of messages. To my surprise, for more fine-grained, heterogeneous workloads, the processing rate significantly dropped.

Still, I had no idea why it would happen, but my bet was in the dependencies. Therefore, I tried one-by-one, reverting the serialization library, database SDK, base Docker image and even config libraries. None of these made any changes.

This made me pull out the big guns. I profiled the service using async-profiler and indeed

CPU profile looked vastly different on Scala 3 than on 2.13.

scala2_13_flamegraph

scala3_flamegraph

JVM-level CPU time was now dominated by JIT compiler while application-level by decoding.

Looking at the top of Scala 3 flamegraph I noticed a long quicklens call.

quicklens_flamegraph

What used to be transparent (frankly, I didn’t even realize we used the library), now took almost half of the total CPU time. I compared how it looks on Scala 2.13 and it was barely noticeable with around 0.5% samples.

Turns out there was indeed a subtle bug making chained evaluations inefficient in Scala 3. This also explained why the JVM spent so much time compiling.

After upgrading the library, performance and CPU characteristics on Scala 3 became indistinguishable from Scala 2.13.

Takeaways

While the details of the bug are pretty interesting(hats off to the SoftwareMill team for catching it!), that’s not my point here. I want to emphasize that libraries can behave very differently between Scala versions, especially when they rely on meta-programming.

Even if your migration is seamless and the service runs fine on Scala 3 - when performance is not just a nice-to-have, do not assume. Know your hotspots and benchmark them. Otherwise, your code will benchmark you, revealing bottlenecks in places you didn’t even know existed.

联系我们 contact @ memedata.com