Rendered at 14:19:28 GMT+0000 (Coordinated Universal Time) with Cloudflare Workers.
liampulles 23 hours ago [-]
Understanding algorithmic complexity (in particular, avoiding rework in loops), is useful in any language, and is sage advice.
In practice though, for most enterprise web services, a lot of real world performance comes down to how efficiently you are calling external services (including the database). Just converting a loop of queries into bulk ones can help loads (and then tweaking the query to make good use of indexes, doing upserts, removing unneeded data, etc.)
I'm hopeful that improvements in LLMs mean we can ditch ORMs (under the guise that they are quicker to write queries and the inbetween mapping code with) and instead make good use of SQL to harness the powers that modern databases provide.
zadikian 18 hours ago [-]
Well before LLMs, I already ditched ORMs. What sometimes holds back SQL is not having a convenient way to call it. Statically-typed languages require you to manually set result types unless you use a compile-time query builder, but that's a whole can of worms. Besides that, many client libs aren't so convenient out of the box, so you still need a few of your own helpers.
Also, before jsonb existed, you'd often run into big blobs of properties you don't care to split up into tables. Now it takes some discipline to avoid shoving things into jsonb that shouldn't be.
It solves all of your issues with “ORMs” (it’s really more than just an ORM)
j-vogel 23 hours ago [-]
Author here. DB and external service calls are often the biggest wins, thanks for calling that out.
In my demo app, the CPU hotspots were entirely in application code, not I/O wait. And across a fleet, even "smaller" gains in CPU and heap compound into real cost and throughput differences. They're different problems, but your point is valid. Goal here is to get more folks thinking about other aspects of performance especially when the software is running at scale.
PathOfEclipse 18 hours ago [-]
My experience profiling is that I/O wait is never the problem. However, the app may actually be spending most of it's CPU time interacting with database. In general, networks have gotten so fast relative to CPU that the CPU cost of marshalling or serializing data across a protocol ends up being the limiting factor. I got a major speedup once just by updating the JSON serialization library an app used.
Seattle3503 21 hours ago [-]
> I'm hopeful that improvements in LLMs mean we can ditch ORMs (under the guise that they are quicker to write queries and the inbetween mapping code with) and instead make good use of SQL to harness the powers that modern databases provide.
Maybe we can ditch active models like those we see in sqlalchemy, but the typed query builders that come with ORMs are going to become more important, not less. Leveraging the compiler to catch bad queries is a huge win.
liampulles 20 hours ago [-]
I use Ecto with Elixir in my day job, and it has a pretty good query building type solution. BUT: I still regularly come into issues where I have to use a fragment in order to do the specific SQL operation that I want, or I start my app and it turns out it has not caught the issue with my query (relating to my specific MySQL version or whatever). Which unfortunately defeats the purpose.
My experience with something like the latest Claude Code models these days has been that they are pretty good at SQL. I think some combination of LLM review of SQL code with smoke tests would do the trick here.
ackfoobar 20 hours ago [-]
> ditch ORMs ... make good use of SQL
I think Java (or other JVM languages) are then best positioned, because of jooq. Still the best SQL generation library I've used.
wcallahan 9 hours ago [-]
Jooq with Kotlin for a back-end has been the best of both worlds for me.
Much cleaner, shorter code and type safety with Postgres (my schema tends to be highly normalized too). And these days I’ve got it well integrated with Zod for type safe JS/TS front-ends as well.
anotherevan 8 hours ago [-]
I'm rather partial to MyBatis (and Liquibase) but I might have to give Jooq a try.
matwood 18 hours ago [-]
Anytime I use a language other than Java it's always jooq that I miss. It's that good.
vincnetas 19 hours ago [-]
thumbs up for jooq
fzeindl 18 hours ago [-]
> a lot of real world performance comes down to how efficiently you are calling external services (including the database)
Apart from that my experience over the last 20 years was that a lot of performance is lost because of memory allocation (in GCed languages like Java or JavaScript). Removing allocation in hot loops really goes a long way and leads to 10 or 100 fold runtime improvements.
kykat 18 hours ago [-]
This has been the key for me as well, memory allocation in hot paths is usually the first optimization that I look for. It's quite surprising to see how far very inefficient algorithms (time complexity wise) can go as long as no allocations are made.
gmueckl 17 hours ago [-]
This applies to non-GC languages as well. Memory management is slow. Even with manual memory management I have been able to dramatically speed up code simply by modifying how memory is allocated.
Parts of the GC language crowd in particular have come to hold some false optimistic beliefs about how well a GC can handle allocations. Also, Java and C# can sneak in silly heap allocations in the wrong places (e.g. autoboxing). So there is a tendency for programs to overload the GC with avoidable work.
mrsmrtss 17 hours ago [-]
Autoboxing is more a Java problem mainly because of type erasure with generics. C# has "proper" generics and no hidden boxing is occuring there.
Mawr 13 hours ago [-]
> Parts of the GC language crowd in particular have come to hold some false optimistic beliefs about how well a GC can handle allocations.
Yep, the idea is "we've made allocations fast, so allocate away!". But that's a trap — every allocation puts pressure on the GC, no matter how fast you've made the very act of allocating. It's a terrible mindset to encourage the users of your language to have.
Then there's the more insidious problem — to make allocations fast you must have traded something off, like GC throughput. So now your GC is slower and encourages programmers to allocate, which makes it even slower.
matwood 18 hours ago [-]
> Just converting a loop of queries into bulk ones can help loads
This is usually the first thing I look for when someone is complaining about speed. Developers often miss it because they are developing against a database on their local machine which removes any of the network latency that exists in deployed environments.
cogman10 22 hours ago [-]
Easy to get wrong as well.
There's a balance with a DB. Doing 1 or 2 row queries 1000 times is obviously inefficient, but making a 1M row query can have it's own set of problems all the same (even if you need that 1M).
It'll depend on the hardware, but you really want to make sure that anything you do with a DB allows for other instances of your application a chance to also interact with the DB. Nothing worse than finding out the 2 row insert is being blocked by a million row read for 20 seconds.
There's also a question of when you should and shouldn't join data. It's not always a black and white "just let the DB handle it". Sometimes the better route to go down is to make 2 queries rather than joining, particularly if it's something where the main table pulls in 1000 rows with only 10 unique rows pulled from the subtable. Of course, this all depends on how wide these things are as well.
But 100% agree, ORMs are the worst way to handle all these things. They very rarely do the right thing out of the box and to make them fast you ultimately end up needing to comprehend the SQL they are emitting in the first place and potentially you end up writing custom SQL anyways.
philipwhiuk 22 hours ago [-]
ORMs are a caching layer for dev time.
They store up conserved programming time and then spend it all at once when you hit the edge case.
If you never hit the case, it's great. As soon as you do, it's all returned with interest :)
xigoi 21 hours ago [-]
The question is why we don’t have database management systems that integrate tightly with the progmming language. Instead we have to communicate between two different paradigms using a textual language, which is itself inefficient.
runroader 21 hours ago [-]
We tried that in 90’s RAD environments like Foxpro and others. If it fits the problem, they were great! If not, it’s even worse than with an ORM.
They rarely fit today since they were all (or mostly) local-first or even local-only. Scaling was either not possible or pretty difficult.
mike_hearn 16 hours ago [-]
https://permazen.io/ exists and is a simpler yet still very powerful way to think about databases (for java but the concepts are general).
But it's only really efficient if it can run code right next to the data via fast access - ideally the same machine. The moment you have a DB running on separate hardware or far away from the client, it's going to be slower.
SQL is a very compact way to communicate what you want from a complex database in a way that can be statically analyzed and dynamically optimized. It's also sandboxable. Not so easily to replace.
Shorel 21 hours ago [-]
Because every single database vendor will try to lock down their users to their DBMS.
Oracle is a prime example of this. Stored procedures are the place to put all business logic according to Oracle documentation.
This caused backslash from escaping developers who then declared business logic should never be inside the database. To avoid vendor lock-in.
There's no ideal solution, just tradeoffs.
cogman10 21 hours ago [-]
> Because every single database vendor will try to lock down their users to their DBMS.
I mean, that already happens. It's quite rare to see someone migrate from one database to another. Even if they stuck to pure SQL for everything, it's still a pretty daunting process as Postgres SQL and MSSQL won't be the same thing.
Shorel 2 hours ago [-]
I migrated a database with some stored procedures from MSSQL Server to Oracle.
Then lots of logic was added as stored procedures to the database.
Then I migrated the same system to MySQL. Including the SP.
Doesn't happen often, but it does happen.
ghurtado 18 hours ago [-]
> It's quite rare to see someone migrate from one database to another.
I'm not discounting the level of effort involved, but I think the reason you don't see this often is because it is rare that simply changing DBMS systems is beneficial in and of itself.
And even if it was frictionless (ie: if we had discovered ORM Samarkanda), the real choices are so limited that even if you did it regularly, you would soon run out of DBMSs to try.
lll-o-lll 6 hours ago [-]
It would obviously be beneficial to go from super expensive to free (Postgres). But no one does - why? Because sql is just a veneer over otherwise two completely different things.
wcallahan 9 hours ago [-]
Isn’t that what Convex is doing?
ivan_gammel 21 hours ago [-]
The answer is simple: model optimized for storage and model designed for processing are two different things. The languages used to describe and query them have to be different.
ghurtado 18 hours ago [-]
> The languages used to describe and query them have to be different.
Absolutely not.
That which is asserted without evidence can be dismissed without evidence.
kerblang 17 hours ago [-]
> Absolutely not.
Can also be dismissed without evidence
liampulles 20 hours ago [-]
I agree with you fully yes. One has to watch out for overwhelmingly large or locking queries.
kykat 18 hours ago [-]
I've been using sqlx + postgres very successfully with claude in the last couple of months. However, we've been raw dogging MySQL and node.js at work for over a year, and I also used raw SQLite from C++ before that (I am still traumatized by all the pointers, never again), so...
laughing_man 19 hours ago [-]
I've always found ORMs to be performance killers. It always worked out better to write the SQL directly. The idea that you should have a one-to-one correspondence between your data objects and your database objects is disastrous unless your data storage is trivial.
chopin 19 hours ago [-]
I worked with ORM (EclipseLink) and used SQL just fine.
When using JDBC I found myself quickly in implementing a poor mans ORM.
nitwit005 15 hours ago [-]
The issue of creating a DB wrapper doesn't go away by using an ORM. One of the complaints about ORMs I have, in practice, is people often create another wrapper around it.
laughing_man 13 hours ago [-]
I saw that a lot, too. I remember one project using Hibernate where the people involved decided to keep the Hibernate objects "pure" and then had them all wrapped in another object they used to keep information that didn't go into the database.
The whole project must have had 3x the number of classes that the actual complexity required, and keeping it all straight was something of a headache. As was onboarding new people, who always struggled with Hibernate.
roegerle 17 hours ago [-]
EclipseLink never received enough love.
sigbottle 21 hours ago [-]
> Understanding algorithmic complexity (in particular, avoiding rework in loops), is useful in any language, and is sage advice.
I recently fixed a treesitter perf issue (for myself) in neovim by just dfsing down the parse tree instead of what most textobject plugins do, which is:
-> walk the entire tree for all subtrees that match this metadata
-> now you have a list of matching subtrees, iterate through said subtree nodes, and see which ones are "close" to your cursor.
But in neovim, when I type "daf", I usually just want to delete the function right under my cursor. So you can just implement the same algorithm by just... dfsing down the parse tree (which has line numbers embedded per nodes) and detecting the matches yourself.
In school, when I did competitive programming and TCS, these gains often came from super clever invariants that you would just sit there for hours, days, weeks, just mulling it over. Then suddenly realize how to do it more cleverly and the entire problem falls away (and a bunch of smart people praise you for being smart :D). This was not one of them - it was just, "go bypass the API and do it faster, but possibly less maintainably".
In industry, it's often trying to manage the tradeoff between readability, maintainability, etc. I'm very much happy to just use some dumb n^2 pattern for n <= 10 in some loop that I don't really care much about, rather than start pulling out some clever state manipulation that could lead to pretty "menial" issues such as:
- accidental mutable variables and duplicating / reusing them later in the code
- when I look back in a week, "What the hell am I doing here?"
- or just tricky logic in general
I only noticed the treesitter textobject issue because I genuinely started working with 1MB autogen C files at work. So... yeah...
I could go and bug the maintainers to expose a "query over text range* API (they only have query, and node text range separately, I believe. At least of the minimal research I have done; I haven't kept up to date with it). But now that ties into considerations far beyond myself - does this expose state in a way that isn't intuitive? Are we adding composable primitives or just ad hoc adding features into the library to make it faster because of the tighter coupling? etc. etc.
I used to think of all of that as just kind of "bs accidentals" and "why shouldn't we just be able to write the best algorithms possible". As a maintainer of some systems now... nah, the architectural design is sometimes more fun!
I may not have these super clever flashes of insight anymore but I feel like my horizons have broadened (though part of it is because GPT Pro started 1 shotting my favorite competitive programming problems circa late 2025 D: )
liampulles 20 hours ago [-]
You are not wrong. There are of course tradeoffs here. There are various things that can improve web service performance, but if we are talking about the performance of a web service in comparison to other more general concerns, like maintainability, then I agree trying to make small performance wins falls pretty low on the list.
After all, even if one has some slow and beastly, unoptimized Spring Boot container that chews through RAM, its not that expenseive (in the grand scheme of things) to just replicate more instances of it.
philipwhiuk 22 hours ago [-]
> external services (including the database)
Or even the local filesystem :)
CPU calls are cheap, memory is pretty cheap, disk is bad, spinning disk is very bad, network is 'good luck'.
You can O(pretty bad) most of the time as long as you stay within the right category of those.
spankalee 22 hours ago [-]
Avoiding Java's string footguns is an interesting problem in programming languages design.
The String.format() problem is most immediately a bad compiler and bad implementation, IMO. It's not difficult to special-case literal strings as the first argument, do parsing at compile time, and pass in a structured representation. The method could also do runtime caching. Even a very small LRU cache would fix a lot of common cases. At the very least they should let you make a formatter from a specific format string and reuse it, like you can with regexes, to explicitly opt into better performance.
But ultimately the string templates proposal should come back and fix this at the language level. Better syntax and guaranteed compile-time construction of the template. The language should help the developer do the fast thing.
String concatenation is a little trickier. In a JIT'ed language you have a lot of options for making a hierarchy of string implementations that optimize different usage patterns, and still be fast - and what you really want for concatenation is a RopeString, like JS VMs have, that simply references the other strings. The issue is that you don't want virtual calls for hot-path string method calls.
Java chose a single final class so all calls are direct. But they should have been able to have a very small sealed class hierarchy where most methods are final and directly callable, and the virtual methods for accessing storage are devirtualized in optimized methods that only ever see one or two classes through a call site.
To me, that's a small complexity cost to make common string patterns fast, instead of requiring StringBuilder.
pfdietz 10 hours ago [-]
It's interesting to see how something like Common Lisp handles the format issue.
In CL, there's a general infrastructure called "compiler macros" that is intended as a hint to the compiler to expand calls as macros at compile time. The macro is also allowed to just leave the form unexpanded, in which case it defaults to an unexpanded function call. And the function can be turned into a value itself and passed around, even if the compiler macro exists.
For CL's format, this means an implementation will typically have a compiler macro (or some similar mechanism) that does an expansion if the format is a string constant.
CL also has a function called formatter that takes a format string and returns a function that acts like (lambda (&rest args) (apply #'format <the format string> args). This function can be implemented as something that expands the format string into code and then compiles the code.
The mechanisms in CL would allow a user to implement the equivalent of a format compiler macro (and formatter) even if the implementation didn't provide them.
rf15 18 hours ago [-]
> But ultimately the string templates proposal should come back and fix this at the language level.
They tried, its opponents dilluted it to the point of uselessness and now will forever use this failed attempt as a wedge.
I'm sorry, I don't believe Java will get sensible String templates in our life time.
brabel 21 hours ago [-]
Yeah, Java is pretty fast despite the fact that it still has these kinds of obviously suboptimal things going on.
I love how Zig, D and Rust do exactly what you say: parse the format string at compile time, making it super efficient at runtime (no parsing, no regex, just the optimal code to get the string you need).
I say this but I write most of my code in Java/Kotlin :D . I just wish I could write more low-level languages for super efficient code, but for what I do, Java is more than enough.
mike_hearn 16 hours ago [-]
Kotlin string interpolation turns into the fancy invokedynamic based string concatenation behind the scenes so it's very optimized: https://openjdk.org/jeps/280
arikrahman 18 hours ago [-]
I admire Rich Hickey's approach of building on top of the Java ecosystem for this reason, adding a functional first approach with emphasis on data structures, where using the right algorithms comes naturally.
jandrewrogers 21 hours ago [-]
> Zig, D and Rust
Also C++, which works the same way.
jiehong 12 hours ago [-]
Perhaps something like zig’s comptime would help a bit.
LtWorf 21 hours ago [-]
Sometimes running strace on jvm software you will see some sycall patterns that are incredibly inefficient.
cmovq 22 hours ago [-]
When you're using a programming language that naturally steers you to write slow code you can't only blame the programmer.
I was listening to someone say they write fast code in Java by avoiding allocations with a PoolAllocator that would "cache" small objects with poolAllocator.alloc(), poolAllocator.release(). So just manual memory management with extra steps. At that point why not use a better language for the task?
andai 18 hours ago [-]
I decompiled Project Zomboid (written in Java) a while back, because I was curious about the performance issues I was having with the game. (Very laggy on my 10 year old laptop, while looking like The Sims 1.) I figured, best case scenario I find some easy bottlenecks and I can patch in a fix.
Well, the whole thing was standard Java OOP, except they also had a bunch of functional programming stuff on top of that. I can relate to that -- I think they were university students when they started, and I definitely had an OOP and FP phase. But then they just... kept it, 10+ years later.
So while it's true that you can write C in any language... those kind of folks don't tend to use Java in the first place ;)
--
(Except Notch? Well, his code looks like C, not sure if it's actually fast! I really enjoyed his 4 kilobyte java games back in the day, I think he published the source for each one too.)
What is the ending of your story!? Did you find and fix some bottlenecks?
ivan_gammel 21 hours ago [-]
TBH, I do not see how Java as a language steers anyone to use one those shotguns. E.g. the knowledge about algorithmic complexity is foundational, the StringBuilder is junior-level basic knowledge.
nightpool 18 hours ago [-]
How would you handle validating numeric input in a hot path then? All of the solutions proposed in #5 are incomplete or broken, and it stems from the fact that Java's language design over-uses exceptions for error handling in places where an optional value would be much safer and faster.
ivan_gammel 18 hours ago [-]
Normally in 100% cases, with parseInt/parseDouble etc. Getting NumberFormatException so frequently on a hot path that it impacts performance means, that you aren’t solving the parsing number problem, you are solving a guessing type problem, which is out of scope for standard library and requires custom parser.
nightpool 18 hours ago [-]
Okay, but this contradicts your original statement that "Java doesn't steer anyone to use these [footguns]". Every language has a way to parse integers, and most developers do not need a custom parser. Only in Java does that suddenly become a performance footgun.
ivan_gammel 18 hours ago [-]
It does not. If you need to parse a number, you use standard library and you will be fine. The described case with huge impact on hot path is the demonstration why using brains is important. The developer that will get into this mess is the one who will find the way to suffocate his code with performance bottlenecks in thousand other ways. It’s not a language or library problem.
dionian 17 hours ago [-]
Yes, parseInt et al work very fast for good inputs. What percentage of your inputs are invalid numbers and why ?
ivan_gammel 17 hours ago [-]
> What percentage of your inputs are invalid numbers and why ?
This is a wrong question to ask in this context. The right question to ask is when actually exceptional flow becomes a performance bottleneck. Because, obviously, in a desktop or even in a server app validating single user input even 99% of wrong inputs won’t cause any trouble. It may become a problem with bulk processing, but then, and I have to repeat myself here, it is no longer a number parsing problem, it’s a problem of not understanding what your input is.
kerblang 16 hours ago [-]
> Java's language design over-uses exceptions for error handling
No, library authors' design over-uses exceptions. Also refer to people using exceptions to generate 404 http responses in web systems - hey, there's an easy DDOS... This can include some of Java's standard libraries, although nothing springs to mind.
Exceptions are not meant for mainstream happy-path execution; they mean that something is broken. Countless times I have had to deal garbage-infested logs where one programmer is using exceptions for rudimentary validation and another is dumping the resulting stack traces left and right as if the world is coming to end.
It is a problem, but it's an abuse problem, not a standard usage problem.
20 hours ago [-]
cxr 19 hours ago [-]
The problem with comments like these is that guessing what "better language" a commentator has in mind is always an exercise left up to the reader. And that tends to be by design—it's great for potshots and punditry, because it means not having make a concrete commitment to anything that might similarly be confronted and torn apart in the replies—like if the "better language" alluded to is C (and it generally is)—the language where the standard library "steers" you towards quadratic string operations because the default/natural way to refer to a string's length is O(n).
ablob 22 hours ago [-]
You might have an application for which speed is not important most of the time.
Only one or two processes might require allocation-free code. For such a case, why would you burden all of the other code with the additional complexity?
Calling out to a different language then may come with baggage you'd rather avoid.
A project might also grow into these requirements. I can easily imagine that something wasn't problematic for a long time but suddenly emerged as an issue over time. At that point you wouldn't want to migrate the whole codebase to a better language anymore.
ekkeke 20 hours ago [-]
This point gets raised every single time managed languages and low latency development come up together. The trade off is running "fast" all of the time, even when you don't have to, vs running slow most of the time and tinkering when you need to go fast.
I've spent a fair few years developing lowish (10-20us wire to wire) latency trading systems and the majority of the code does not need to go fast. It's just wasted effort, a debugging headache, and technical debt. So the natural trade off is a bit of pain to make the hot path fast through spans, unsafe code, pre-allocated object pools, etc and in return you get to use a safe and easy programming language everywhere else.
In C# low latency dev is not even that painful, as there are a lot of tools available specifically for this purpose by the runtime.
laughing_man 19 hours ago [-]
Java doesn't steer you into object pools. I wrote Java code for 20 years and never used a cache to avoid allocating objects, and never saw a colleague use one. The person you were talking to doesn't know what he's doing.
cogman10 22 hours ago [-]
Bad idea. I've made a pool allocator before, but that was for expensive network objects and expensive objects dealing with JNI.
Doing it to avoid memory pressure generally means you simply have a bad algorithm that needs to be tweaked. It's very rarely the right solution.
gf000 16 hours ago [-]
Not sure why you are down voted. Depending on how its used it could actually be detrimental to performance.
The JVM may optimize many short lived objects better than a pool of objects with less reasonably lifetimes.
cogman10 16 hours ago [-]
This is the second time this week on HN that I've seen people suggesting object pools to solve memory pressure problems.
I generally think it's because people aren't experienced with diagnosing and fixing memory pressure. It's one of the things I do pretty frequently for my day job. I'm fortunate enough to be the "performance" guy at work :).
It'll always depend on what the real issue is, but generally speaking the problem to solve isn't reinventing garbage collection, but rather to eliminate the reason for the allocation.
For example, a pretty common issue I've seen is copying a collection to do transformations. Switching to streams, combining transformation operations, or in an extreme case, I've found passing around a consumer object was the way to avoid a string of collection allocations.
Even the case where small allocations end up killing performance, for example like the autoboxing example of the OP, often the solution is to either make something mutable that isn't, or to switch to primitives (Valhalla can't come soon enough).
Heck, sometimes even an object cache is the right solution. I've had good success reducing the size of objects on the heap by creating things like `Map<String, String>` and then doing a `map.computeIfAbsent(str, Function.identity());` (Yes, I know about string interning, no I don't want these added to the global intern cache).
Regardless, the first step is profiling (JFRs and heap dumps) to see where memory is spent and what is dominating the allocation rate. That's a first step that people often skip and jump straight to fixing what they think is broken.
kykat 18 hours ago [-]
I saw something like that being suggested when working with GIS data with many points as classes in Java, the object overhead for storing XYZ doubles is quite crazy. The optimization was to build a global double array and use "pointers" to get and set the number in the array.
Even JavaScript is much better for this, much, much better.
spankalee 17 hours ago [-]
JavaScript has the exact same issue - objects are on the heap and require allocation and pointer dereferencing. For huge collections of numbers, arrays might be better.
But JS has another problem: there's no way to force a number to be unboxed (no primitive vs boxed types), so the array of doubles might very well be an array of pointers to numbers[1].
But with hidden class optimizations an object might be able to store a float directly in a field, so the array of objects will have one box per (x,y,z), while an array of "numbers" might have one box per number, so 3x as many. My guess is, without benchmarking, is that JS is much worse than Java then, because the "optimization" will end up being worse.
[1]: Most JS engines have an optimization for small ints, called SMIs, that use pointer tagging to support either an int or a references, but I don't think they typically do this optimization for floats.
lern_too_spel 18 hours ago [-]
You're describing array of structs vs. struct of arrays. Even in JavaScript, you would have to manually do the latter.
kykat 18 hours ago [-]
V8 automatically optimizes objects with the same shape into efficient structs, making array of objects much more efficient than in Java.
And the manually manager int array acts more like system memory, it's not continuous, so you could have point i 0 and 2 and the data would be:
[1, 2, 3, x, x,x, 3, 2, 1] (3D points).
So I am not describing a struct of arrays.
spankalee 17 hours ago [-]
Hidden class optimizations just make JavaScript objects behave a little more like Java class instances, where the VM knows where to find each field, rather than having to look it up like a map.
It doesn't make JS faster than Java, it makes it almost as fast in some cases.
kykat 16 hours ago [-]
I can only say what I observed in testing, and that's that having millions of instances of a class like Point3D{x, y, z} in JS uses significantly less memory than in Java (this was tested on Android, not sure if relevant). It was quite some time ago so I don't remember the details.
gf000 7 hours ago [-]
Well, Android is not running Java, it runs Android runtime (dalvik) byte code. In general, depending on when it was, that runtime is much much simpler and does a lot more at compile time at the expense of less JIT optimization.
It's also many versions behind the Java API (depending on when it happened).
So your data point is basically completely irrelevant.
The optimization you discussed for GIS data is called struct of arrays. JavaScript does not do that automatically for you. You would have to do the same thing manually in JavaScript to avoid per-triple object overhead.
d_burfoot 19 hours ago [-]
> So just manual memory management with extra steps
This is actually the perfect situation: you are allowed to do it carefully and manually for 1% of code on the hot path, but you don't have to worry about it for the 99% of the code that's not.
wiseowise 15 hours ago [-]
> At that point why not use a better language for the task?
Such as?
devnotes77 19 hours ago [-]
[dead]
cogman10 22 hours ago [-]
Nitpick just because.
Orders by hour could be made faster. The issue with it is it's using a map when an array works both faster and just fine.
On top of that, the map boxes the "hour" which is undesirable.
This is how I'd write it
long[] ordersByHour = new long[24];
var deafultTimezone = ZoneId.systemDefault();
for (Order order : orders) {
int hour = order.timestamp().atZone(deafultTimezone).getHour();
ordersByHour[hour]++;
}
If you know the bound of an array, it's not large, and you are directly indexing in it, you really can't do any better performance wise.
It's also not less readable, just less familiar as Java devs don't tend to use arrays that much.
maybe it would be a little better to use ints rather than longs, as Java lists can't be bigger than the int max value anyways. Saves you a cache line or two.
cogman10 22 hours ago [-]
Fair point, but it is possible this isn't a list but rather some sort of iterable. Those can be boundless.
Practically speaking, that would be pretty unusual. I don't think I've ever seen that sort of construct in my day to day coding (which could realistically have more than 1B elements).
kyrra 24 hours ago [-]
First request latency also can really suck in Java before hotpathed code gets through the C2 compiler. You can warm up hotpaths by running that code during startup, but it's really annoying having to do that. Using C++, Go, or Rust gets you around that problem without having to jump through the hoops of code path warmup.
I wish Java had a proper compiler.
pron 23 hours ago [-]
You mostly need a recent JDK. Leyden has already cut down warmup by a lot and is expected to continue driving it down.
You can create a native executable with GraalVM. Alternatively, if you want to keep the JVM: With the ongoing project Leyden, you can already "pre-train" some parts of the JVM warm-up, with full AoT code compilation coming some time in the future.
Thaxll 22 hours ago [-]
GraalVM has a lot of limitations, some popular lib don't work with it. From what I remember anything using reflection is painful to use.
senkora 23 hours ago [-]
And going the other direction, if you want your C++ binaries to benefit from statistics about how to optimize the steady-state behavior of a long-running process, the analogous technique is profile-guided optimization (PGO).
vbezhenar 22 hours ago [-]
GraalVM is terrible. Eats gigabytes of memory to compile super simple application. Spends minutes doing that. If you need compiled native app, just use Golang.
brabel 21 hours ago [-]
I used to be really excited about GraalVM but this, together with limitations in what Java code can run (reflection must be whitelisted - i.e. pain) made me run away from it. I do use Go, but my favourite substitute for Java is actually Dart. It can run as a script, compile to a binary or to a multiplatform "fast" format (a bit like a jar), and performance wise it's par on par with Java! It's faster on some things, a bit slower on other... but in general, compiling to exe makes it extremely fast to start, like Go. I think it even shares some Go binary creation tooling since both are made by Google and I remember when they were implementing the native compiler, they mentioned something about that.
gf000 7 hours ago [-]
But you only need to do that before a release. You can just develop with the normal JVM with ultra fast incremental builds and hot reload.
titzer 23 hours ago [-]
I worked on JVMs long ago (almost twenty years now). At that time most Java usage was for long-running servers. The runtime team staunchly refused to implement AOT caching for as long as possible. This was a huge missed opportunity for Java, as client startup time has always, always, always sucked. Only in the past 3-5 years does it seem like things have started to shift, in part due to the push for Graal native image.
I long ago concluded that Java was not a client or systems programming language because of the implementation priorities of the JVM maintainers. Note that I say priorities--they are extremely bright and capable engineers that focus on different use cases, and there isn't much money to be made from a client ecosystem.
bob1029 23 hours ago [-]
AOT is nice for startup time, but there are tradeoffs in the other direction for long tail performance issues in production.
There are JITs that use dynamic profile guided optimization which can adjust the emitted binary at runtime to adapt to the real world workload. You do not need to have a profile ahead of time like with ordinary PGO. Java doesn't have this yet (afaik), but .NET does and it's a huge deal for things like large scale web applications.
Java definitely does have that, although maybe not in the most commonly used JDK distributions. E.g. companies needing ultra-low latency even before the first request, would use Azul JDKs like Prime (paid), pre-train the profile in non-production environments, and then use it in production on new version deployment.
hrmtst93837 19 hours ago [-]
Gaming the JIT just to get startup times in line is a decent sign that Java's "fast" comes with invisible asterisks all over prod graphs. At some point you're managing the runtime, not the app.
AOT options like GraalVM Native Image can help cold starts a lot, but then half your favorite frameworks breaks and you trade one set of hoops for another. Pick which pain you want.
taeric 23 hours ago [-]
I challenge the idea that first request latency is bottle necked by language choice. I can see how that is plausible, mind. Is it a concern for the vast majority of developers?
pjmlp 23 hours ago [-]
Excelsior JET, now gone, but only because GraalVM and OpenJ9 exist now.
The folks on embedded get to play with PTC and Aicas.
Android, even if not proper Java, has dex2oat.
a-dub 23 hours ago [-]
i'd be curious about a head to head comparison of how much the c2 actually buys over a static aot compilation with something serious like llvm.
if it is valuable, i'd be surprised you can't freeze/resume the state and use it for instantaneous workload optimized startup.
They do, to add to another comment of mine elsewhere, JIT caches go all the way back to products like JRockit, and IBM JVM has and it for years in Maestro, now available as OpenJ9.
Too many folks have this mindset there is only one JVM, when that has never been the case since the 2000's, after Java for various reasons started poping everywhere.
rileymichael 23 hours ago [-]
the best way is via CRaC (https://docs.azul.com/crac/) but only a few vendors support it and there’s a bit of process to get it setup.
in practice, for web applications exposing some sort of `WarmupTask` abstraction in your service chassis that devs can implement will get you quite far. just delay serving traffic on new deployments until all tasks complete. that way users will never hit a cold node
user3939382 23 hours ago [-]
My architecture builds a command registry in Clojure/JVM which runs as a daemon, the registry is shared by a dynamically generated babashka (GraalVM) shell that only includes whitelisted commands for that user. So for the user, unauthorized commands don’t even exist, and I get my JVM app with no startup overhead.
dionian 23 hours ago [-]
This is why I use java for long running processes, if i care about a small binary that launches fast, i just use something slower at runtime but faster at startup like python.
packetlost 23 hours ago [-]
Python startup time can be pretty abysmal too if you have a lot of imports.
And then you get applications choosing the worst of both worlds, like bazel/blaze.
belfthrow 23 hours ago [-]
I really hate how completely clueless people on hn are about java. This is not, and has not been an issue for many many years in Java and even the most junior of developers know how to avoid it. But oh no, go and rust is alwaayssss the solution sure.
pythonaut_16 23 hours ago [-]
Can you provide any examples or evidence of Java apps that prove this?
Because in my experience as of 2026, Java programs are consistently among the most painful or unpleasant to interact with.
belfthrow 21 hours ago [-]
Crac / aot cache / ready now all can address this. Not even considering native aot. Multiple low latency trading systems across market markers, hedge funds and ibs prove this. But people just want to compare it to building a cli tool in go or rust.
pythonaut_16 19 hours ago [-]
But like can you provide an actual example of an application?
> But people just want to compare it to building a cli tool in go or rust.
This seems like the key. HN is definitely biased towards simpler, smaller tools. (And that's not a bad thing!). The most compelling JVM stories I hear are all from much larger scale enterprise settings.
Kafka being a good example. It's very good at what it does, but painful to manage and usually not worth the pain for anyone who's not in a mega enterprise.
belfthrow 16 hours ago [-]
Because in real life, real world applications software is large, long running and needs to be bulletproof. Clis are not powering the world's infrastructure via piped bash scripts. It really baffles me what people actually do as software engineers on here with some of the nonsense that gets thrown around.
dionian 17 hours ago [-]
IntelliJ IDEA is reasonably fast, but of course its hard to make a big desktop app in java be fast.
bombcar 23 hours ago [-]
Ah, but let's port rust to the JVM!
seu 19 hours ago [-]
I'm a bit surprised to see those examples, because there's nothing really new here. These are typical beginner pitfalls and have been there for at least a decade or more. Or maybe it's because I learned java in the late 90s and later used it for J2ME, and then using things like StringBuilder (StringBuffer in the old days) were almost mandatory, and you would be very careful trying to avoid unnecessary object allocations.
drtse4 8 hours ago [-]
Yes, but that's not the path that modern frameworks suggest nowadays.
larsnystrom 19 hours ago [-]
I remember writing Java for our introductory programming course at university around 2010. I was already familiar with object oriented programming in PHP at the time, so I just wrote the Java code like I would write PHP. I was absolutely astounded at the poor performance of the Java app. I asked one of our tutors and I can still remember him looking at the code and saying something along the lines of ”oh, you’re instantiating objects in a loop, that’s obviously going to be slow”. Like, what? If I can do this performantly in freakin PHP, how can Java, the flagship of OOP, not have fast instantiation of objects? I’m still shaking my head thinking about it.
zahlman 18 hours ago [-]
Did you actually benchmark that task against similarly-architected PHP code?
lern_too_spel 17 hours ago [-]
Your tutor misdiagnosed the issue. These allocations in a tight loop would have used bump allocation on Java 6 in 2010, and the young generation would have used a copy collector, which would have freed those objects more cheaply than any unspecialized malloc/free. It would have beaten the pants off of PHP's reference counting GC.
EricRiese 18 hours ago [-]
This is a Spring specific gripe and I know this blog post doesn't assume Spring, but I hate seeing `new ObjectMapper()`. Spring Boot auto configures an ObjectMapper for you and you probably want the customization it gives you, including `java.time` handling and classpath scanning. I've wrestled with so many bugs caused by not using the `ObjectMapper` bean.
drtse4 8 hours ago [-]
If you really want to write really performant java code, the word "spring" should not even be mentioned. Same thing for Jackson, write you own lazy json library if the data is bigger than a few 100k.
The code will not look pretty but it will be very fast.
computerdork 16 hours ago [-]
Have always really liked Java, but yeah, Spring overall has been terrible for the language. Autowiring is against the principles of a typesafe programming language - Don't make me guess what what object is going to be attached to a reference. And if you do, at least figure out what linked object is at compile time, not at run time.
Spring autowiring makes Java seem as a whole unnecessarily complex. Think it should be highly discouraged in the language (unless it is revamped and made apart of the compiler).
... not sure how this applies to the ObjectMapper, as I haven't programmed in Java in awhile.
... and my gripe doesn't apply to SpringBoot though:)
hiddew 6 hours ago [-]
> Autowiring is against the principles of a typesafe programming language
Constructor autowiring is the application of the inversion of control and dependency injection pattern. If there was no autowiring, you could autowire the components together just the same with normal code calling constructors in the correct order. Spring just finds the components and does the construction for you.
dionian 17 hours ago [-]
well yeah Jackson is slow.
tombert 17 hours ago [-]
I think that the `sychronized` keyword in Java was a mistake.
I've seen classes that are meant to be used by multiple threads where literally every method has `synchronized` because "that was the only way they could get it to work". Of course, if literally every method is synchronized it literally can't actually be used by multiple threads, it just looks like it is.
Generally speaking I work pretty hard to avoid any kind of locks. Locks can be an anti-pattern in my mind: for a lot of problems, if I am reaching for a lock, it's because I haven't actually thought through the problems well enough. They're a bandaid and they create potential choke-points in the app. I also think that they're a crappy fix to try and shoehorn non-concurrent patterns into a concurrent landscape.
I personally think that making something thread-safe and concurrent while also being maintainable and fast is a hard problem, and I think lazily trying to add threads into concurrent applications is a good way to write terrible code that is impossible to debug.
Obviously no accounting for taste, but when I write programs now, I kind of always make them concurrent-first (generally using and/or reinventing the actor model). I try and build my initial algorithm to accept that concurrency is inevitable and start that from the get go. I can't remember the last time I reached for `synchronized`, though every now and then I do have to reach for ReentrantLock, and I always feel dirty doing so.
drtse4 8 hours ago [-]
A huge mistake, I've seen a lot of code where clearly the author thought that just adding "synchronized" would have solved any concurrency issue. And no one even talks about how synchronized is implemented, basically a monitor on the object.
The point of view is usually also wrong, they focus on the method call flow while they should think about protecting access to shared data.
drtse4 8 hours ago [-]
0. Fix your algorithms, code optimization comes later
1. Avoid abstraction as much as possible, convoluted flow control and reduce useless objects creation
2. Learn how to manage concurrency correctly, focus on the data being accessed by multiple thread and focus on sequential access
3. Don't use bloated frameworks (all of them)
4. Consider rewriting common libraries following the principles above and with only the functionalities you actually need.
Easy 10x improvement, try it.
wood_spirit 23 hours ago [-]
A subject close to my heart, I write a lot of heavily optimised code including a lot of hot data pipelines in Java.
And aside from algorithms, it usually comes down to avoiding memory allocations.
I have my go-to zero-alloc grpc and parquet and json and time libs etc and they make everything fast.
It’s mostly how idiomatic Java uses objects for everything that makes it slow overall.
But eventually after making a JVM app that keeps data in something like data frames etc and feels a long way from J2EE beans you can finally bump up against the limits that only c/c++/rust/etc can get you past.
polothesecond 23 hours ago [-]
> And aside from algorithms, it usually comes down to avoiding memory allocations.
I’ve heard about HFT people using Java for workloads where micro optimization is needed.
To be frank, I just never understood it. From what I’ve seen heard/you have to write the code in such a way that makes it look clumsy and incompatible with pretty much any third party dependencies out there.
And at that point, why are you even using Java? Surely you could use C, C++, or any variety of popular or unpopular languages that would be more fitting and ergonomic (sorry but as a language Java just feels inferior to C# even). The biggest swelling point of Java is the ecosystem, and you can’t even really use that.
yunnpp 19 hours ago [-]
I am very interested about this and would like an authoritative answer on this. I even went as far as buying some books on code optimization in the context of HFT and I was not impressed. Not a single snippet of assembly; how are you optimizing anything if you don't look at what the compiler produces?
But on Java specifically: every Java object still has a 24-byte overhead. How doesn't that thrash your cache?
The advice on avoiding allocations in Java also results in terrible code. For example, in math libraries, you'll often see void Add(Vector3 a, Vector3 b, Vector3 our) as opposed to the more natural Vector3 Add(Vector3 a, Vector3 b). There you go, function composition goes out the window and the resulting code is garbage to read and write. Not even C is that bad; the compiler will optimize the temporaries away. So you end up with Java that is worse than a low-level imperative language.
And, as far as I know, the best GC for Java still incurs no less than 1ms pauses? I think the stock ones are as bad as 10ms. How anyone does low-latency anything in Java then boggles my mind.
yrxuthst 17 hours ago [-]
Modern ZGC guarantees under 1ms pause times, and Azul's pauseless C4 has been around for a while too.
speed_spread 7 hours ago [-]
The biggest selling point of Java is that you can easily find programmers that know it. They will need some training to do HFT style code but you'll still pay them less than C++ prima donnas and they'll churn out reasonably robust code in good time.
dikaflowt 22 hours ago [-]
Can you share the libs you 're using?
kpw94 22 hours ago [-]
The Autoboxing example imo is a case of "Java isn't so fast". Why can't this be optimized behind the scenes by the compiler ?
Rest of advice is great: things compilers can't really catch but a good code reviewer should point out.
kllrnohj 22 hours ago [-]
javac for better or worse is aggressively against doing optimizations to the point of producing the most ridiculously bad code. The belief tends to be that the JIT will do a better job fixing it if it has byte code that's as close as possible to the original code. But this only helps if a) the code ever gets JIT'd at all (rarely true for eg class initializers), and b) the JIT has the budget to do that optimization. Although JITs have the advantage of runtime information, they are also under immense pressure to produce any optimizations as fast as possible. So they rarely do the level of deep optimizations of an offline compiler.
vbezhenar 22 hours ago [-]
Why should compiler optimize obviously dumb code? If developer wants to create billions of heap objects, compiler should respect him. Optimizing dumb code is what made C++ unbearable. When you write one code and compilers generates completely different code.
carlmr 22 hours ago [-]
The problem is rather that Java doesn't have generics and structs, so you're kind of forced to box things or can't use collections.
layer8 19 hours ago [-]
For #5, the “fix” [0] is incomplete, because you will still get a NumberFormatException when the value is out of range. For int, you could check if there are more or less than 10 digits, and use parseLong() when there are exactly 10 digits. For long, you can use BigInteger when there are exactly 18 digits. After skipping any leading zeros, of course. Or you could just replicate the JDK’s parsing implementation and change the part where it throws NumberFormatException (at the possible cost of foregoing JIT intrinsics).
A second bug is that Character.isDigit() returns true for non-ASCII Unicode digits as well, while Integer.parseInt() only supports ASCII digits.
Another bug is that the code will fail on the input string "-".
Lastly, using value.isBlank() is a pessimization over value.isEmpty() (or just checking value.length(), which is read anyway in the next line), given that the loop would break on the first blank character. It makes the function not be constant-time, along with the first point above that the length of the digit sequence isn’t being limited.
[0]
public int parseOrDefault(String value, int defaultValue) {
if (value == null || value.isBlank()) return defaultValue;
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
if (i == 0 && c == '-') continue;
if (!Character.isDigit(c)) return defaultValue;
}
return Integer.parseInt(value);
}
pregnenolone 18 hours ago [-]
Java really isn't fast unless you're basically writing C style Java. Abstracting is expensive in Java and if one can't abstract, what's the point of writing Java in 2026? After all these years value types aka Valhalla are still in a soon™ state. If it weren't for Project Loom, which is genuinely awesome, I wouldn’t see any justification for using Java in this day and age.
jimbokun 17 hours ago [-]
Compared to what?
fnord77 17 hours ago [-]
The world has shown that it doesn't need all those intricate design patterns that java fosters.
It does seem like java missed their chance, it's a shame.
dionian 17 hours ago [-]
my hotspot JITd code is quite fast
Izkata 22 hours ago [-]
"Java is slow" is a reputation it earned in the 90s/2000s because the JVM startup (at least on Windows) was extremely slow, like several seconds, with a Java-branded splash screen during that time. Even non-technical people made the association.
marginalia_nu 19 hours ago [-]
Java's start up speeds were greatly improved by the engineers at Oracle, but thankfully we invented springboot and guice to get around that problem.
Okx 22 hours ago [-]
The code:
public int parseOrDefault(String value, int defaultValue) {
if (value == null || value.isBlank()) return defaultValue;
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
if (i == 0 && c == '-') continue;
if (!Character.isDigit(c)) return defaultValue;
}
return Integer.parseInt(value);
}
Is probably worse than Integer.parseInt alone, since it can still throw NumberFormatExceptions for values that overflow (which is no longer handled!). Would maybe fix that. Unfortunately this is a major flaw in the Java standard library; parsing numbers shouldn't throw expensive exceptions.
j-vogel 18 hours ago [-]
Good catches from several of you. The original fix dropped the try-catch entirely which was a regression for overflow and edge cases like a bare "-". Updated the post to keep a try-catch around the final parseInt as a safety net. The pre-validation still avoids the expensive path for the common cases, which is the core point. Appreciate the feedback.
zahlman 18 hours ago [-]
Not to mention it's extra work in the case where the input usually is valid.
deepsun 19 hours ago [-]
And it will fail with "-"
titzer 23 hours ago [-]
For fillInStackTrace, another trick is to define your own Exception subclass and override the method to be empty. I learned this trick 15+ years ago.
It doesn't excuse the "use exceptions for control flow" anti-pattern, but it is a quick patch.
ivan_gammel 21 hours ago [-]
God, please make me unsee it. That‘s a cool trick that turns into an anti-pattern itself if abused.
zvqcMMV6Zcr 23 hours ago [-]
> Exceptions for Control Flow
This one is so prevalent that JVM has an optimization where it gives up on filling stack for exception, if it was thrown over and over in exact same place.
j-vogel 23 hours ago [-]
Author here. Great callout. That's the -XX:+OmitStackTraceInFastThrow optimization, been around since JDK 5. The C2 compiler detects exceptions thrown repeatedly from the same site and starts reusing a preallocated instance without filling the stack trace. Good for performance, but it makes debugging harder in production since you lose the trace. You can disable it with -XX:-OmitStackTraceInFastThrow if you need the traces back.
dust-jacket 21 hours ago [-]
ah, thank you. Haven't worked in java for a bit now, but that was the only one I read where I was like "I'm sure we didn't have to avoid this when I worked on java".
The rest were all very familiar. Well, apart from the new stuff. I think most of my code was running in java 6...
zmmmmm 18 hours ago [-]
String concatenation in a loop is a 1990's era Java footgun. It's interesting the things that have persisted vs been cast aside. Very significant design decisions have been enforced on far less grounds than the stupidity of how the default String concatenation operator works.
IsTom 16 hours ago [-]
Does Java not have some kind of helper function to join strings? Why'd anyone write loops like that?
inglor_cz 16 hours ago [-]
This. I learnt Java in 2004-5, when 1.4 was dominant and O'Reilly books were a much better training resource than whatever you found on the Internet.
Concatenating of thousands of individual Strings was already considered a well-known performance killer back then.
Interesting to see this in the wild in 2026.
hiyer 22 hours ago [-]
I ran into 5 and 7 in a Flink app recently - was parsing a timestamp as a number first and then falling back to iso8601 string, which is what it was. The flamegraph showed 10% for the exception handling bit.
While fixing that, also found repeated creation of datetimeformatter.
Both were not in loops, but both were being done for every event, for 10s of 1000s of events every second.
Thanks! I'm using Instant.parse at present and this is supposedly 37x faster. Will definitely give it a try.
wood_spirit 7 hours ago [-]
And report back please! :)
sgbeal 21 hours ago [-]
Slight correction:
> StringBuilder works off a single mutable character buffer. One allocation.
It's one allocation to instantiate the builder and _any_ number of allocations after that (noting that it's optimized to reduce allocations, so it's not allocating on every append() unless they're huge).
jerf 22 hours ago [-]
Any non-trivial program that has never had an optimizer run on it has a minimal-effort 50+% speedup in it.
uraura 22 hours ago [-]
I thought those were common sense until I worked on a program written by my colleague recently.
comrade1234 23 hours ago [-]
Also finding the right garbage collector and settings that works best for your project can help a lot.
taspeotis 23 hours ago [-]
Knock Knock
Who’s there?
long pause
Java
ackfoobar 20 hours ago [-]
The premise of this joke is dead since 2020, when ZGC was production ready.
jandrewrogers 23 hours ago [-]
You can write many of the bad examples in the article in any language. It is just far more common to see them in Java code than some other languages.
Java is only fast-ish even on its best day. The more typical performance is much worse because the culture around the language usually doesn't consider performance or efficiency to be a priority. Historically it was even a bit hostile to it.
this_user 23 hours ago [-]
Performance is really not Java's issue. Even bad Java code is still substantially faster than the bulk of modern software that is based on technologies like Python or JavaScript/Node.js.
KronisLV 5 hours ago [-]
This might also be why I heard colleagues saying “Nono, listen, these ‘N+1 problems’ and our nested service calls aren’t an issue because it works well enough” until it eventually didn’t. I’d rather not have bad code in any language.
Modern Java runtimes are pretty good, though.
array_key_first 9 hours ago [-]
No - this is not entirely true. Many of Java's fundamental design decisions lead to unexpected slowness that just does not happen in other languages.
For example, appending to a string in a loop. That only happens because of how Java handles strings. In C++, that's very fast. As fast as it can get, really. Since it all goes into the same buffer that gets mutated and expands at a good growth rate. Basically, equivalent to StringBuilder, but that's just all strings.
Or, boxing. C++ doesn't have to box generics to store them in a container.
gf000 2 hours ago [-]
> For example, appending to a string in a loop. That only happens because of how Java handles strings. In C++, that's very fast. As fast as it can get, really. Since it all goes into the same buffer that gets mutated and expands at a good growth rate
Well everything comes at a price. Just imagine how many billions of very hard to debug bugs did Java save the world from by going immutable on Strings, at the expense of some slow down when used without a though, and pretty much every java book starts with how to avoid it since decades?
steve1977 23 hours ago [-]
Which, to be fair, in many cases is ok. If you just need to churn out LOB apps for worker drones as cheap as possible, performance is probably not the most important factor.
coldtea 2 hours ago [-]
>Same app. Same tests. Same JDK.
Same AI slop.
ww520 22 hours ago [-]
The autoboxing in a loop case can be handled by the compiler.
bearjaws 24 hours ago [-]
JavaScript can be fast too, it's just the ecosystem and decisions devs make that slow it down.
Same for Java, I have yet to in my entire career see enterprise Java be performant and not memory intensive.
At the end of the day, if you care about performance at the app layer, you will use a language better suited to that.
maccard 24 hours ago [-]
My experience with the defaults in JavaScript is that they’re pretty slow. It’s really, really easy to hit the limits of an express app and for those limits to be in your app code. I’ve worked on JVM backed apps and they’re memory hungry (well, they require a reallocation for the JVM) and they’re slow to boot but once they’re going they are absolutely ripping fast and your far more likely to be bottlenecked by your DB long before you need to start doing any horizontal scaling.
wiradikusuma 23 hours ago [-]
Compile it to native (GraalVM) and you can get it fast while consuming less memory. But now your build is slow :)
maccard 23 hours ago [-]
The minute a project has maven in it the build is slow. Don’t even get me started on Gradle…
j-vogel 24 hours ago [-]
Fair point on ecosystem decisions, that's basically the thesis of the post. These patterns aren't Java being slow, they're developers (myself included) writing code that looks fine but works against the JVM. Enterprise Java gets a bad rap partly because these patterns compound silently across large codebases and nobody profiles until something breaks.
FatherOfCurses 23 hours ago [-]
"Enterprise Java"
Factories! Factories everywhere!
wood_spirit 23 hours ago [-]
Yes! Obligatory link to the seminal work on the subject:
Why do you think this plays out over and over again? What's the causal mechanisms of this strange attractor
pron 18 hours ago [-]
Well, JS is fast and Go is faster, but Java is C++-fast.
Mawr 12 hours ago [-]
What a ridiculous claim. You're either deluded or outright lying.
zahlman 18 hours ago [-]
> Accidental O(n²) with Streams Inside Loops
Man that code looks awful. Really reminds me of why I drifted away from Java over time. Not just the algorithm, of course; the repetitiveness, the hoops that you have to jump through in order to do pretty "stream processing"... and then it's not even an FP algorithm in the end, either way!
Honestly the only time I can imagine the "process the whole [collection] per iteration" thing coming up is where either you really do need to compare (or at least really are intentionally comparing) each element to each other element, or else this exact problem of building a histogram. And for the latter I honestly haven't seen people fully fall into this trap very often. More commonly people will try to iterate over the possible buckets (here, hour values), sometimes with a first pass to figure out what those might be. That's still extra work, but at least it's O(kn) instead of O(n^2).
You can do this sort of thing in an elegant, "functional" looking way if you sort the data first and then group it by the same key. That first pass is O(n lg n) if you use a classical sort; making a histogram like this in the first place is basically equivalent to radix sort, but it's nice to not have to write that yourself. I just want to show off what it can look like e.g. in Python:
def local_hour(order):
return datetime.datetime.fromtimestamp(order.timestamp).hour
groups = itertools.groupby(sorted(orders, key=local_hour), key=local_hour)
orders_by_hour = {hour: len(list(orders)) for (hour, orders) in groups}
Anyway, overall I feel like these kinds of things are mostly done by people who don't need to have the problem explained, who have simply been lazy or careless and simply need to be made to look in the right place to see the problem. Cf. Dan Luu's anecdotes https://danluu.com/algorithms-interviews/ , and I can't seem to find it right now but the story about saving a company millions of dollars finding Java code that was IIRC resizing an array one element at a time.
(Another edit: originally I missed that the code was only trying to count the number of orders in each hour, rather than collecting them. I fixed the code above, but the discussion makes less sense for the simplified problem. In Python we can do this with `collections.Counter`, but it wouldn't be unreasonable to tally things up in a pre-allocated `counts_by_hour = [0] * 24` either.)
----
Edit:
> String.format() came in last in every category. It has to... StringBuilder was consistently the fastest. The fix: [code not using StringBuilder]... Use String.format() for the numeric formatting where you need it, and let the compiler optimize the rest. Or just use a StringBuilder if you need full control.
Yeah, this is confused in a way that I find fairly typical of LLM output. The attitude towards `String.format` is just plain inconsistent. And there's no acknowledgment of how multiple `+`s in a line get optimized behind the scenes. And the "fix" still uses `String.format` to format the floating-point value, and there's no investigation of what that does to performance or whether it can be avoided.
latchkey 21 hours ago [-]
When they say that AI will replace programmers, I think of this article and come to terms with my own job security.
Most of this stuff is just central knowledge of the language that you pick up over time. Certainly, AI can also pick this stuff up instantly, but will it always pick the most efficient path when generating code for you?
Probably not, until we get benchmarks into the hot path of our test suite. That is something someone should work on.
victor106 23 hours ago [-]
this is great, so practical!!!
any other resources like this?
null-phnix 23 hours ago [-]
[dead]
ryguz 23 hours ago [-]
[dead]
abitabovebytes 20 hours ago [-]
[dead]
andrewmcwatters 23 hours ago [-]
[dead]
r_lee 24 hours ago [-]
[flagged]
spwa4 21 hours ago [-]
Java IS fast. The time between deciding to use Java and Oracle's lawyers breaking down your door is measured in just weeks these days.
gf000 2 hours ago [-]
Jokes are only funny when they have an ounce of truth to them.
Oracle was the one who open-sourced the whole of the JDK, and is the main contributor to OpenJDK by far, which is completely open-source with the same license as the Linux kernel. It's fine to criticize them on e.g. Oracle db licenses and stuff like that, but they have been excellent stewards of Java and all the bad language around this java lawyering stuff is just FUD.
tripple6 24 hours ago [-]
Do good, don't do bad. Okay.
abound 23 hours ago [-]
I don't think that's a charitable take of the article. To many programmers, it wouldn't be obvious that some of these footguns (autoboxing, string concatenation, etc) are "bad", or what the "good" alternatives are (primitives, StringBuilder, etc).
That said, the article does have the "LLM stank" on it, which is always offputting, but the content itself seems solid.
koakuma-chan 24 hours ago [-]
As much as I love Java, everybody should just be using Rust. That way you are actually in control, know what's going on, etc. Another reason specifically against Java is that the tooling, both Maven and Gradle, still stucks.
piva00 24 hours ago [-]
Gradle does suck, it gives too much freedom on a tool that should be straightforward and actively design to avoid footguns, it does the opposite by providing a DSL that can create a lot of abstractions to manage dependencies. The only place I worked where the Gradle configuration looked somewhat sane had very strict design guidelines on what was acceptable to be in the Gradle config.
Maven on the other hand, is just plain boring tech that works. There's plenty of documentation on how to use it properly for many different environments/scenarios, it's declarative while enabling plug-ins for bespoke customisations, it has cruft from its legacy but it's quite settled and it just works.
Could Maven be more modern if it was invented now? Yeah, sure, many other package managers were developed since its inception with newer/more polished concepts but it's dependable, well documented, and it just plain works.
computerdork 16 hours ago [-]
Just wrote a comment how I've always liked Maven. It's perfect for small and medium sized projects, and for service-oriented architectures/microservices - it seems like it was designed for this! It's main goal is to help you figure out the libraries that you're using and build them in a standard way.
It isn't great for really strange and odd builds, but in that case, you should probably be breaking your project down into smaller components (each with it's own maven file) anyways.
koakuma-chan 23 hours ago [-]
I would disagree that either "plain works" because to even package your app into a self-contained .jar, you need a plugin. I can't recall the specifics now, but years ago I spent many hours fighting both Maven and Gradle.
piva00 23 hours ago [-]
Well, yes? It's a feature provided by a plugin, like any other feature in Maven, you declare the plugin for creating a fat-jar or single-jar and use that. It's just some lines of XML configuration so it plain works.
Like I said, it's not hypermodern with batteries included, and streamlined for what became more common workflows after it was created but it doesn't need workarounds, it's not complicated to define a plugin to be called in one of the steps of the lifecycle, and it's provided as part of its plugin architecture.
I can understand spending many hours fighting Gradle, even I with plenty of experience with Gradle (begrudgingly, I don't like it at all) still end up fighting its idiocies but Maven... It's like any other tool, you need to learn the basics but after that you will only fight it if you are verging away from the well-documented usage (which are plenty, it's been battle-tested for decades).
looperhacks 23 hours ago [-]
You "need a plugin" in the sense that every component of maven is a "plugin". The core plugins give you everything you need to build a self-contained jar - if you wanted to, you don't even have to configure the plugins, if you want to write a long cli command instead.
jayd16 23 hours ago [-]
Not knowing what's going on in Java is a personal problem. The language and jvm have its own quirks but it's no less knowable than any other compiler optimized code.
The debugging and introspection tooling in Java is also best in class so I would say it's one of the more understandable run times.
Gradle does suck and maven is ok but a bit ugly.
computerdork 16 hours ago [-]
Actually, really like maven, it's focus on building in standard way is fantastic (but agreed, it look messy, with all its xml and necessary versioning).
dionian 23 hours ago [-]
LLMs take the whole argument away. Yes, maven/gradle/sbt suck to work with. But now you can just generate it.
spopejoy 2 hours ago [-]
LOL I wish. LLMs massacre gradle code all the time. Once you're past boilerplate generation and doing anything remotely unusual they can't stop hallucinating broken shit that they insist works.
computerdork 16 hours ago [-]
Actually, I like Maven. It's perfect for code that is broken into medium-sized projects, which makes it great for service-oriented architectures (would have said microservices here instead, but think we're learning that breaking our services too finely down is generally not a good idea).
Yeah, it seems like Maven is designed to build just one project with relatively little build-code (although, figuring out versioning of the libs used in your build can get tricky, but guessing this is how it is in most languages). It's still one of my favorites build tools for many situations.
dionian 17 hours ago [-]
I've been using maven for 20+ years, gradle for 10? ant for 5 before that. sbt for 15. I've written custom plugins for all of them. I know them quite well, unfortunately.
I use LLMs to maintain them now. I keep the build files simple. It was an inconvenience before, but a trifle now.
ActorNightly 21 hours ago [-]
Lets look at Java in modern day.
* Most mature Java project has moved to Kotlin.
* The standard build system uses gradle, which is either groovy or kotlin, which gets compiled to java which then compiles java.
* Log4shell, amongst other vulnerabilities.
* Super slow to adopt features like async execution
* Standard repo usage is terrible.
There is no point in using Java anymore. I don't agree that Rust is a replacement, but between Python, Node, and C/C++ extensions to those, you can do everything you need.
gf000 2 hours ago [-]
> Most mature Java project has moved to Kotlin.
Demonstrably false, not even close
Re Gradle using groovy/kotlin: so what? Gradle is not a standard any more than Maven, and java is not primary used as a scripting language, so it makes sense that it has a different language for its config files? What's the deal here?
Show me a language without vulnerabilities.
It has virtual threads for quite some times and it is a much much better choice for most use cases than async.
shermantanktop 23 hours ago [-]
I’ll never understand the impulse to tell the entire world what to do based on your own personal preferences and narrow experiences.
It gets a reaction, though, so great for social media.
pjmlp 23 hours ago [-]
Rust has no place other than deployment scenarios where any kind of automatic resource management, be it tracing GC or reference counting, is not wanted for, either due to technical reasons, or being a waste of time trying to change people's mindset.
j-vogel 24 hours ago [-]
I'm a fan of Rust too. But there are millions of Java applications running in production right now, and some of them are running these anti-patterns today. Not everyone has the option to rewrite in a different language. For those teams, knowing what to look for in a profiler can make a real difference without changing a single dependency.
koakuma-chan 24 hours ago [-]
I think that right now it is easier than ever to rewrite your app in Rust, due to LLMs. Unfortunately there are still people out there who dismiss this idea, and continue having their back-end written in much inferior languages, like JavaScript or Python. If your back-end is written in Java, you aren't even in the worst spot.
kykat 18 hours ago [-]
"You think" is cheap, try doing it, rewrite an existing library in rust and see how it goes. Doing a rough prototype is easy, but the real work starts after that.
23 hours ago [-]
krona 24 hours ago [-]
> That way you are actually in control
Programming in Rust is a constant negotiation with the compiler. That isn't necessarily good or bad but I have far more control in Zig, and flexibility in Java.
koakuma-chan 24 hours ago [-]
Yes, there is a learning curve to Rust, but once you get proficient, it no longer bothers you. I think this is more good than bad, because, for example, look at Bun, it is written in Zig, it has so many bugs. They had a bug in their filesystem API that freezed your process, and it stayed unfixed for at least half a year after I filed it. Zig is a nice C replacement, but it doesn't have the same correctness guardrails as Rust.
krona 23 hours ago [-]
Assuming we're talking about the same bug, The filesystem API freeze wasn't caused by Zig's lack of correctness guarantees, but a design flaw in Bun's implementation.
dryarzeg 23 hours ago [-]
Maybe I'm stupid, but I never actually understood people who blame programming languages for bugs in software. Because sure, it's good to have guardrails, but in my opinion, if you're writing a program and there's a bug, unless this bug lies somewhere in implementation of compiler/interpreter/etc, you can't blame the tooling, It's you who introduced this bug. It was your mistake.
It's cool when your tooling warns you about potential bugs or mistakes in implementation, but it's still your responsibility to write the correct code. If you pick up a hammer and hit your finger instead of the nail, then in most cases (though not always) it’s your own fault.
chuckadams 23 hours ago [-]
When millions of users constantly make the same mistake with the tool, there may be a problem with the tool, whether it's a defect in the tool or just that it's inappropriate for the job. Blaming the user might give one a righteous feeling, but decade after decade that approach has failed to actually fix any problems.
dryarzeg 23 hours ago [-]
That's why I say "in most cases" - so not always, actually. There might be problems with tools, I'm not trying to deny that. And by the way, what if some (or even most) of the users just don't have enough skill to use the tool properly? Again, there could be a problem with tool, yes, but you can't always blame only tools for mistakes users make.
I am talking about this bug. It looks like it is still unfixed, in the sense, there is a PR fixing it, but it wasn't merged. LOL.
Regardless of whether this specific bug would be caught by Rust compiler, Bun in general is notorious for crashing, just look at how many open issues there are, how many crashes.
Not saying that you cannot make a correct program in Zig, but I prefer having checks that Rust compiler does, to not having them.
In practice though, for most enterprise web services, a lot of real world performance comes down to how efficiently you are calling external services (including the database). Just converting a loop of queries into bulk ones can help loads (and then tweaking the query to make good use of indexes, doing upserts, removing unneeded data, etc.)
I'm hopeful that improvements in LLMs mean we can ditch ORMs (under the guise that they are quicker to write queries and the inbetween mapping code with) and instead make good use of SQL to harness the powers that modern databases provide.
Also, before jsonb existed, you'd often run into big blobs of properties you don't care to split up into tables. Now it takes some discipline to avoid shoving things into jsonb that shouldn't be.
https://learn.microsoft.com/en-us/dotnet/csharp/linq/
It solves all of your issues with “ORMs” (it’s really more than just an ORM)
In my demo app, the CPU hotspots were entirely in application code, not I/O wait. And across a fleet, even "smaller" gains in CPU and heap compound into real cost and throughput differences. They're different problems, but your point is valid. Goal here is to get more folks thinking about other aspects of performance especially when the software is running at scale.
Maybe we can ditch active models like those we see in sqlalchemy, but the typed query builders that come with ORMs are going to become more important, not less. Leveraging the compiler to catch bad queries is a huge win.
My experience with something like the latest Claude Code models these days has been that they are pretty good at SQL. I think some combination of LLM review of SQL code with smoke tests would do the trick here.
I think Java (or other JVM languages) are then best positioned, because of jooq. Still the best SQL generation library I've used.
Much cleaner, shorter code and type safety with Postgres (my schema tends to be highly normalized too). And these days I’ve got it well integrated with Zod for type safe JS/TS front-ends as well.
Apart from that my experience over the last 20 years was that a lot of performance is lost because of memory allocation (in GCed languages like Java or JavaScript). Removing allocation in hot loops really goes a long way and leads to 10 or 100 fold runtime improvements.
Parts of the GC language crowd in particular have come to hold some false optimistic beliefs about how well a GC can handle allocations. Also, Java and C# can sneak in silly heap allocations in the wrong places (e.g. autoboxing). So there is a tendency for programs to overload the GC with avoidable work.
Yep, the idea is "we've made allocations fast, so allocate away!". But that's a trap — every allocation puts pressure on the GC, no matter how fast you've made the very act of allocating. It's a terrible mindset to encourage the users of your language to have.
Then there's the more insidious problem — to make allocations fast you must have traded something off, like GC throughput. So now your GC is slower and encourages programmers to allocate, which makes it even slower.
This is usually the first thing I look for when someone is complaining about speed. Developers often miss it because they are developing against a database on their local machine which removes any of the network latency that exists in deployed environments.
There's a balance with a DB. Doing 1 or 2 row queries 1000 times is obviously inefficient, but making a 1M row query can have it's own set of problems all the same (even if you need that 1M).
It'll depend on the hardware, but you really want to make sure that anything you do with a DB allows for other instances of your application a chance to also interact with the DB. Nothing worse than finding out the 2 row insert is being blocked by a million row read for 20 seconds.
There's also a question of when you should and shouldn't join data. It's not always a black and white "just let the DB handle it". Sometimes the better route to go down is to make 2 queries rather than joining, particularly if it's something where the main table pulls in 1000 rows with only 10 unique rows pulled from the subtable. Of course, this all depends on how wide these things are as well.
But 100% agree, ORMs are the worst way to handle all these things. They very rarely do the right thing out of the box and to make them fast you ultimately end up needing to comprehend the SQL they are emitting in the first place and potentially you end up writing custom SQL anyways.
They store up conserved programming time and then spend it all at once when you hit the edge case.
If you never hit the case, it's great. As soon as you do, it's all returned with interest :)
But it's only really efficient if it can run code right next to the data via fast access - ideally the same machine. The moment you have a DB running on separate hardware or far away from the client, it's going to be slower.
SQL is a very compact way to communicate what you want from a complex database in a way that can be statically analyzed and dynamically optimized. It's also sandboxable. Not so easily to replace.
Oracle is a prime example of this. Stored procedures are the place to put all business logic according to Oracle documentation.
This caused backslash from escaping developers who then declared business logic should never be inside the database. To avoid vendor lock-in.
There's no ideal solution, just tradeoffs.
I mean, that already happens. It's quite rare to see someone migrate from one database to another. Even if they stuck to pure SQL for everything, it's still a pretty daunting process as Postgres SQL and MSSQL won't be the same thing.
I'm not discounting the level of effort involved, but I think the reason you don't see this often is because it is rare that simply changing DBMS systems is beneficial in and of itself.
And even if it was frictionless (ie: if we had discovered ORM Samarkanda), the real choices are so limited that even if you did it regularly, you would soon run out of DBMSs to try.
Absolutely not.
That which is asserted without evidence can be dismissed without evidence.
Can also be dismissed without evidence
When using JDBC I found myself quickly in implementing a poor mans ORM.
The whole project must have had 3x the number of classes that the actual complexity required, and keeping it all straight was something of a headache. As was onboarding new people, who always struggled with Hibernate.
I recently fixed a treesitter perf issue (for myself) in neovim by just dfsing down the parse tree instead of what most textobject plugins do, which is:
-> walk the entire tree for all subtrees that match this metadata
-> now you have a list of matching subtrees, iterate through said subtree nodes, and see which ones are "close" to your cursor.
But in neovim, when I type "daf", I usually just want to delete the function right under my cursor. So you can just implement the same algorithm by just... dfsing down the parse tree (which has line numbers embedded per nodes) and detecting the matches yourself.
In school, when I did competitive programming and TCS, these gains often came from super clever invariants that you would just sit there for hours, days, weeks, just mulling it over. Then suddenly realize how to do it more cleverly and the entire problem falls away (and a bunch of smart people praise you for being smart :D). This was not one of them - it was just, "go bypass the API and do it faster, but possibly less maintainably".
In industry, it's often trying to manage the tradeoff between readability, maintainability, etc. I'm very much happy to just use some dumb n^2 pattern for n <= 10 in some loop that I don't really care much about, rather than start pulling out some clever state manipulation that could lead to pretty "menial" issues such as:
- accidental mutable variables and duplicating / reusing them later in the code
- when I look back in a week, "What the hell am I doing here?"
- or just tricky logic in general
I only noticed the treesitter textobject issue because I genuinely started working with 1MB autogen C files at work. So... yeah...
I could go and bug the maintainers to expose a "query over text range* API (they only have query, and node text range separately, I believe. At least of the minimal research I have done; I haven't kept up to date with it). But now that ties into considerations far beyond myself - does this expose state in a way that isn't intuitive? Are we adding composable primitives or just ad hoc adding features into the library to make it faster because of the tighter coupling? etc. etc.
I used to think of all of that as just kind of "bs accidentals" and "why shouldn't we just be able to write the best algorithms possible". As a maintainer of some systems now... nah, the architectural design is sometimes more fun!
I may not have these super clever flashes of insight anymore but I feel like my horizons have broadened (though part of it is because GPT Pro started 1 shotting my favorite competitive programming problems circa late 2025 D: )
After all, even if one has some slow and beastly, unoptimized Spring Boot container that chews through RAM, its not that expenseive (in the grand scheme of things) to just replicate more instances of it.
Or even the local filesystem :)
CPU calls are cheap, memory is pretty cheap, disk is bad, spinning disk is very bad, network is 'good luck'.
You can O(pretty bad) most of the time as long as you stay within the right category of those.
The String.format() problem is most immediately a bad compiler and bad implementation, IMO. It's not difficult to special-case literal strings as the first argument, do parsing at compile time, and pass in a structured representation. The method could also do runtime caching. Even a very small LRU cache would fix a lot of common cases. At the very least they should let you make a formatter from a specific format string and reuse it, like you can with regexes, to explicitly opt into better performance.
But ultimately the string templates proposal should come back and fix this at the language level. Better syntax and guaranteed compile-time construction of the template. The language should help the developer do the fast thing.
String concatenation is a little trickier. In a JIT'ed language you have a lot of options for making a hierarchy of string implementations that optimize different usage patterns, and still be fast - and what you really want for concatenation is a RopeString, like JS VMs have, that simply references the other strings. The issue is that you don't want virtual calls for hot-path string method calls.
Java chose a single final class so all calls are direct. But they should have been able to have a very small sealed class hierarchy where most methods are final and directly callable, and the virtual methods for accessing storage are devirtualized in optimized methods that only ever see one or two classes through a call site.
To me, that's a small complexity cost to make common string patterns fast, instead of requiring StringBuilder.
In CL, there's a general infrastructure called "compiler macros" that is intended as a hint to the compiler to expand calls as macros at compile time. The macro is also allowed to just leave the form unexpanded, in which case it defaults to an unexpanded function call. And the function can be turned into a value itself and passed around, even if the compiler macro exists.
For CL's format, this means an implementation will typically have a compiler macro (or some similar mechanism) that does an expansion if the format is a string constant.
CL also has a function called formatter that takes a format string and returns a function that acts like (lambda (&rest args) (apply #'format <the format string> args). This function can be implemented as something that expands the format string into code and then compiles the code.
The mechanisms in CL would allow a user to implement the equivalent of a format compiler macro (and formatter) even if the implementation didn't provide them.
They tried, its opponents dilluted it to the point of uselessness and now will forever use this failed attempt as a wedge.
I'm sorry, I don't believe Java will get sensible String templates in our life time.
I love how Zig, D and Rust do exactly what you say: parse the format string at compile time, making it super efficient at runtime (no parsing, no regex, just the optimal code to get the string you need).
I say this but I write most of my code in Java/Kotlin :D . I just wish I could write more low-level languages for super efficient code, but for what I do, Java is more than enough.
Also C++, which works the same way.
I was listening to someone say they write fast code in Java by avoiding allocations with a PoolAllocator that would "cache" small objects with poolAllocator.alloc(), poolAllocator.release(). So just manual memory management with extra steps. At that point why not use a better language for the task?
Well, the whole thing was standard Java OOP, except they also had a bunch of functional programming stuff on top of that. I can relate to that -- I think they were university students when they started, and I definitely had an OOP and FP phase. But then they just... kept it, 10+ years later.
So while it's true that you can write C in any language... those kind of folks don't tend to use Java in the first place ;)
--
(Except Notch? Well, his code looks like C, not sure if it's actually fast! I really enjoyed his 4 kilobyte java games back in the day, I think he published the source for each one too.)
EDIT: Found it!
https://web.archive.org/web/20120317121029/http://www.mojang...
Edit 2: This one has a download, still works!
https://web.archive.org/web/20120301015921/http://www.mojang...
This is a wrong question to ask in this context. The right question to ask is when actually exceptional flow becomes a performance bottleneck. Because, obviously, in a desktop or even in a server app validating single user input even 99% of wrong inputs won’t cause any trouble. It may become a problem with bulk processing, but then, and I have to repeat myself here, it is no longer a number parsing problem, it’s a problem of not understanding what your input is.
No, library authors' design over-uses exceptions. Also refer to people using exceptions to generate 404 http responses in web systems - hey, there's an easy DDOS... This can include some of Java's standard libraries, although nothing springs to mind.
Exceptions are not meant for mainstream happy-path execution; they mean that something is broken. Countless times I have had to deal garbage-infested logs where one programmer is using exceptions for rudimentary validation and another is dumping the resulting stack traces left and right as if the world is coming to end.
It is a problem, but it's an abuse problem, not a standard usage problem.
A project might also grow into these requirements. I can easily imagine that something wasn't problematic for a long time but suddenly emerged as an issue over time. At that point you wouldn't want to migrate the whole codebase to a better language anymore.
I've spent a fair few years developing lowish (10-20us wire to wire) latency trading systems and the majority of the code does not need to go fast. It's just wasted effort, a debugging headache, and technical debt. So the natural trade off is a bit of pain to make the hot path fast through spans, unsafe code, pre-allocated object pools, etc and in return you get to use a safe and easy programming language everywhere else.
In C# low latency dev is not even that painful, as there are a lot of tools available specifically for this purpose by the runtime.
Doing it to avoid memory pressure generally means you simply have a bad algorithm that needs to be tweaked. It's very rarely the right solution.
The JVM may optimize many short lived objects better than a pool of objects with less reasonably lifetimes.
I generally think it's because people aren't experienced with diagnosing and fixing memory pressure. It's one of the things I do pretty frequently for my day job. I'm fortunate enough to be the "performance" guy at work :).
It'll always depend on what the real issue is, but generally speaking the problem to solve isn't reinventing garbage collection, but rather to eliminate the reason for the allocation.
For example, a pretty common issue I've seen is copying a collection to do transformations. Switching to streams, combining transformation operations, or in an extreme case, I've found passing around a consumer object was the way to avoid a string of collection allocations.
Even the case where small allocations end up killing performance, for example like the autoboxing example of the OP, often the solution is to either make something mutable that isn't, or to switch to primitives (Valhalla can't come soon enough).
Heck, sometimes even an object cache is the right solution. I've had good success reducing the size of objects on the heap by creating things like `Map<String, String>` and then doing a `map.computeIfAbsent(str, Function.identity());` (Yes, I know about string interning, no I don't want these added to the global intern cache).
Regardless, the first step is profiling (JFRs and heap dumps) to see where memory is spent and what is dominating the allocation rate. That's a first step that people often skip and jump straight to fixing what they think is broken.
Even JavaScript is much better for this, much, much better.
But JS has another problem: there's no way to force a number to be unboxed (no primitive vs boxed types), so the array of doubles might very well be an array of pointers to numbers[1].
But with hidden class optimizations an object might be able to store a float directly in a field, so the array of objects will have one box per (x,y,z), while an array of "numbers" might have one box per number, so 3x as many. My guess is, without benchmarking, is that JS is much worse than Java then, because the "optimization" will end up being worse.
[1]: Most JS engines have an optimization for small ints, called SMIs, that use pointer tagging to support either an int or a references, but I don't think they typically do this optimization for floats.
And the manually manager int array acts more like system memory, it's not continuous, so you could have point i 0 and 2 and the data would be: [1, 2, 3, x, x,x, 3, 2, 1] (3D points).
So I am not describing a struct of arrays.
It doesn't make JS faster than Java, it makes it almost as fast in some cases.
It's also many versions behind the Java API (depending on when it happened).
So your data point is basically completely irrelevant.
The optimization you discussed for GIS data is called struct of arrays. JavaScript does not do that automatically for you. You would have to do the same thing manually in JavaScript to avoid per-triple object overhead.
This is actually the perfect situation: you are allowed to do it carefully and manually for 1% of code on the hot path, but you don't have to worry about it for the 99% of the code that's not.
Such as?
Orders by hour could be made faster. The issue with it is it's using a map when an array works both faster and just fine.
On top of that, the map boxes the "hour" which is undesirable.
This is how I'd write it
If you know the bound of an array, it's not large, and you are directly indexing in it, you really can't do any better performance wise.It's also not less readable, just less familiar as Java devs don't tend to use arrays that much.
Practically speaking, that would be pretty unusual. I don't think I've ever seen that sort of construct in my day to day coding (which could realistically have more than 1B elements).
I wish Java had a proper compiler.
https://foojay.io/today/how-is-leyden-improving-java-perform...
https://quarkus.io/blog/leyden-1/
I long ago concluded that Java was not a client or systems programming language because of the implementation priorities of the JVM maintainers. Note that I say priorities--they are extremely bright and capable engineers that focus on different use cases, and there isn't much money to be made from a client ecosystem.
There are JITs that use dynamic profile guided optimization which can adjust the emitted binary at runtime to adapt to the real world workload. You do not need to have a profile ahead of time like with ordinary PGO. Java doesn't have this yet (afaik), but .NET does and it's a huge deal for things like large scale web applications.
https://devblogs.microsoft.com/dotnet/bing-on-dotnet-8-the-i...
AOT options like GraalVM Native Image can help cold starts a lot, but then half your favorite frameworks breaks and you trade one set of hoops for another. Pick which pain you want.
The folks on embedded get to play with PTC and Aicas.
Android, even if not proper Java, has dex2oat.
if it is valuable, i'd be surprised you can't freeze/resume the state and use it for instantaneous workload optimized startup.
I mean, both of your points are a thing, see https://www.azul.com/products/components/falcon-jit-compiler... for LLVM as a JIT compiler
and https://openjdk.org/jeps/483 (and in general, project Leyden)
Too many folks have this mindset there is only one JVM, when that has never been the case since the 2000's, after Java for various reasons started poping everywhere.
in practice, for web applications exposing some sort of `WarmupTask` abstraction in your service chassis that devs can implement will get you quite far. just delay serving traffic on new deployments until all tasks complete. that way users will never hit a cold node
There are options to turn on which cause the JVM to save off and reload compiled classes. It pretty massively improves performance.
You can get even faster if you do that plus doing a jlink jvm. But that's more of a pain. The AOT cache is a lot simpler to do.
https://openjdk.org/jeps/514
Because in my experience as of 2026, Java programs are consistently among the most painful or unpleasant to interact with.
> But people just want to compare it to building a cli tool in go or rust.
This seems like the key. HN is definitely biased towards simpler, smaller tools. (And that's not a bad thing!). The most compelling JVM stories I hear are all from much larger scale enterprise settings.
Kafka being a good example. It's very good at what it does, but painful to manage and usually not worth the pain for anyone who's not in a mega enterprise.
The code will not look pretty but it will be very fast.
Spring autowiring makes Java seem as a whole unnecessarily complex. Think it should be highly discouraged in the language (unless it is revamped and made apart of the compiler).
... not sure how this applies to the ObjectMapper, as I haven't programmed in Java in awhile. ... and my gripe doesn't apply to SpringBoot though:)
Constructor autowiring is the application of the inversion of control and dependency injection pattern. If there was no autowiring, you could autowire the components together just the same with normal code calling constructors in the correct order. Spring just finds the components and does the construction for you.
I've seen classes that are meant to be used by multiple threads where literally every method has `synchronized` because "that was the only way they could get it to work". Of course, if literally every method is synchronized it literally can't actually be used by multiple threads, it just looks like it is.
Generally speaking I work pretty hard to avoid any kind of locks. Locks can be an anti-pattern in my mind: for a lot of problems, if I am reaching for a lock, it's because I haven't actually thought through the problems well enough. They're a bandaid and they create potential choke-points in the app. I also think that they're a crappy fix to try and shoehorn non-concurrent patterns into a concurrent landscape.
I personally think that making something thread-safe and concurrent while also being maintainable and fast is a hard problem, and I think lazily trying to add threads into concurrent applications is a good way to write terrible code that is impossible to debug.
Obviously no accounting for taste, but when I write programs now, I kind of always make them concurrent-first (generally using and/or reinventing the actor model). I try and build my initial algorithm to accept that concurrency is inevitable and start that from the get go. I can't remember the last time I reached for `synchronized`, though every now and then I do have to reach for ReentrantLock, and I always feel dirty doing so.
The point of view is usually also wrong, they focus on the method call flow while they should think about protecting access to shared data.
1. Avoid abstraction as much as possible, convoluted flow control and reduce useless objects creation
2. Learn how to manage concurrency correctly, focus on the data being accessed by multiple thread and focus on sequential access
3. Don't use bloated frameworks (all of them)
4. Consider rewriting common libraries following the principles above and with only the functionalities you actually need.
Easy 10x improvement, try it.
And aside from algorithms, it usually comes down to avoiding memory allocations.
I have my go-to zero-alloc grpc and parquet and json and time libs etc and they make everything fast.
It’s mostly how idiomatic Java uses objects for everything that makes it slow overall.
But eventually after making a JVM app that keeps data in something like data frames etc and feels a long way from J2EE beans you can finally bump up against the limits that only c/c++/rust/etc can get you past.
I’ve heard about HFT people using Java for workloads where micro optimization is needed.
To be frank, I just never understood it. From what I’ve seen heard/you have to write the code in such a way that makes it look clumsy and incompatible with pretty much any third party dependencies out there.
And at that point, why are you even using Java? Surely you could use C, C++, or any variety of popular or unpopular languages that would be more fitting and ergonomic (sorry but as a language Java just feels inferior to C# even). The biggest swelling point of Java is the ecosystem, and you can’t even really use that.
But on Java specifically: every Java object still has a 24-byte overhead. How doesn't that thrash your cache?
The advice on avoiding allocations in Java also results in terrible code. For example, in math libraries, you'll often see void Add(Vector3 a, Vector3 b, Vector3 our) as opposed to the more natural Vector3 Add(Vector3 a, Vector3 b). There you go, function composition goes out the window and the resulting code is garbage to read and write. Not even C is that bad; the compiler will optimize the temporaries away. So you end up with Java that is worse than a low-level imperative language.
And, as far as I know, the best GC for Java still incurs no less than 1ms pauses? I think the stock ones are as bad as 10ms. How anyone does low-latency anything in Java then boggles my mind.
Rest of advice is great: things compilers can't really catch but a good code reviewer should point out.
A second bug is that Character.isDigit() returns true for non-ASCII Unicode digits as well, while Integer.parseInt() only supports ASCII digits.
Another bug is that the code will fail on the input string "-".
Lastly, using value.isBlank() is a pessimization over value.isEmpty() (or just checking value.length(), which is read anyway in the next line), given that the loop would break on the first blank character. It makes the function not be constant-time, along with the first point above that the length of the digit sequence isn’t being limited.
[0]
It does seem like java missed their chance, it's a shame.
It doesn't excuse the "use exceptions for control flow" anti-pattern, but it is a quick patch.
This one is so prevalent that JVM has an optimization where it gives up on filling stack for exception, if it was thrown over and over in exact same place.
The rest were all very familiar. Well, apart from the new stuff. I think most of my code was running in java 6...
Concatenating of thousands of individual Strings was already considered a well-known performance killer back then.
Interesting to see this in the wild in 2026.
> StringBuilder works off a single mutable character buffer. One allocation.
It's one allocation to instantiate the builder and _any_ number of allocations after that (noting that it's optimized to reduce allocations, so it's not allocating on every append() unless they're huge).
Who’s there?
long pause
Java
Java is only fast-ish even on its best day. The more typical performance is much worse because the culture around the language usually doesn't consider performance or efficiency to be a priority. Historically it was even a bit hostile to it.
Modern Java runtimes are pretty good, though.
For example, appending to a string in a loop. That only happens because of how Java handles strings. In C++, that's very fast. As fast as it can get, really. Since it all goes into the same buffer that gets mutated and expands at a good growth rate. Basically, equivalent to StringBuilder, but that's just all strings.
Or, boxing. C++ doesn't have to box generics to store them in a container.
Well everything comes at a price. Just imagine how many billions of very hard to debug bugs did Java save the world from by going immutable on Strings, at the expense of some slow down when used without a though, and pretty much every java book starts with how to avoid it since decades?
Same AI slop.
Same for Java, I have yet to in my entire career see enterprise Java be performant and not memory intensive.
At the end of the day, if you care about performance at the app layer, you will use a language better suited to that.
Factories! Factories everywhere!
https://gwern.net/doc/cs/2005-09-30-smith-whyihateframeworks...
Man that code looks awful. Really reminds me of why I drifted away from Java over time. Not just the algorithm, of course; the repetitiveness, the hoops that you have to jump through in order to do pretty "stream processing"... and then it's not even an FP algorithm in the end, either way!
Honestly the only time I can imagine the "process the whole [collection] per iteration" thing coming up is where either you really do need to compare (or at least really are intentionally comparing) each element to each other element, or else this exact problem of building a histogram. And for the latter I honestly haven't seen people fully fall into this trap very often. More commonly people will try to iterate over the possible buckets (here, hour values), sometimes with a first pass to figure out what those might be. That's still extra work, but at least it's O(kn) instead of O(n^2).
You can do this sort of thing in an elegant, "functional" looking way if you sort the data first and then group it by the same key. That first pass is O(n lg n) if you use a classical sort; making a histogram like this in the first place is basically equivalent to radix sort, but it's nice to not have to write that yourself. I just want to show off what it can look like e.g. in Python:
Anyway, overall I feel like these kinds of things are mostly done by people who don't need to have the problem explained, who have simply been lazy or careless and simply need to be made to look in the right place to see the problem. Cf. Dan Luu's anecdotes https://danluu.com/algorithms-interviews/ , and I can't seem to find it right now but the story about saving a company millions of dollars finding Java code that was IIRC resizing an array one element at a time.(Another edit: originally I missed that the code was only trying to count the number of orders in each hour, rather than collecting them. I fixed the code above, but the discussion makes less sense for the simplified problem. In Python we can do this with `collections.Counter`, but it wouldn't be unreasonable to tally things up in a pre-allocated `counts_by_hour = [0] * 24` either.)
----
Edit:
> String.format() came in last in every category. It has to... StringBuilder was consistently the fastest. The fix: [code not using StringBuilder]... Use String.format() for the numeric formatting where you need it, and let the compiler optimize the rest. Or just use a StringBuilder if you need full control.
Yeah, this is confused in a way that I find fairly typical of LLM output. The attitude towards `String.format` is just plain inconsistent. And there's no acknowledgment of how multiple `+`s in a line get optimized behind the scenes. And the "fix" still uses `String.format` to format the floating-point value, and there's no investigation of what that does to performance or whether it can be avoided.
Most of this stuff is just central knowledge of the language that you pick up over time. Certainly, AI can also pick this stuff up instantly, but will it always pick the most efficient path when generating code for you?
Probably not, until we get benchmarks into the hot path of our test suite. That is something someone should work on.
any other resources like this?
Oracle was the one who open-sourced the whole of the JDK, and is the main contributor to OpenJDK by far, which is completely open-source with the same license as the Linux kernel. It's fine to criticize them on e.g. Oracle db licenses and stuff like that, but they have been excellent stewards of Java and all the bad language around this java lawyering stuff is just FUD.
That said, the article does have the "LLM stank" on it, which is always offputting, but the content itself seems solid.
Maven on the other hand, is just plain boring tech that works. There's plenty of documentation on how to use it properly for many different environments/scenarios, it's declarative while enabling plug-ins for bespoke customisations, it has cruft from its legacy but it's quite settled and it just works.
Could Maven be more modern if it was invented now? Yeah, sure, many other package managers were developed since its inception with newer/more polished concepts but it's dependable, well documented, and it just plain works.
It isn't great for really strange and odd builds, but in that case, you should probably be breaking your project down into smaller components (each with it's own maven file) anyways.
Like I said, it's not hypermodern with batteries included, and streamlined for what became more common workflows after it was created but it doesn't need workarounds, it's not complicated to define a plugin to be called in one of the steps of the lifecycle, and it's provided as part of its plugin architecture.
I can understand spending many hours fighting Gradle, even I with plenty of experience with Gradle (begrudgingly, I don't like it at all) still end up fighting its idiocies but Maven... It's like any other tool, you need to learn the basics but after that you will only fight it if you are verging away from the well-documented usage (which are plenty, it's been battle-tested for decades).
Gradle does suck and maven is ok but a bit ugly.
Yeah, it seems like Maven is designed to build just one project with relatively little build-code (although, figuring out versioning of the libs used in your build can get tricky, but guessing this is how it is in most languages). It's still one of my favorites build tools for many situations.
I use LLMs to maintain them now. I keep the build files simple. It was an inconvenience before, but a trifle now.
* Most mature Java project has moved to Kotlin.
* The standard build system uses gradle, which is either groovy or kotlin, which gets compiled to java which then compiles java.
* Log4shell, amongst other vulnerabilities.
* Super slow to adopt features like async execution
* Standard repo usage is terrible.
There is no point in using Java anymore. I don't agree that Rust is a replacement, but between Python, Node, and C/C++ extensions to those, you can do everything you need.
Demonstrably false, not even close
Re Gradle using groovy/kotlin: so what? Gradle is not a standard any more than Maven, and java is not primary used as a scripting language, so it makes sense that it has a different language for its config files? What's the deal here?
Show me a language without vulnerabilities.
It has virtual threads for quite some times and it is a much much better choice for most use cases than async.
It gets a reaction, though, so great for social media.
Programming in Rust is a constant negotiation with the compiler. That isn't necessarily good or bad but I have far more control in Zig, and flexibility in Java.
It's cool when your tooling warns you about potential bugs or mistakes in implementation, but it's still your responsibility to write the correct code. If you pick up a hammer and hit your finger instead of the nail, then in most cases (though not always) it’s your own fault.
I am talking about this bug. It looks like it is still unfixed, in the sense, there is a PR fixing it, but it wasn't merged. LOL.
Regardless of whether this specific bug would be caught by Rust compiler, Bun in general is notorious for crashing, just look at how many open issues there are, how many crashes.
Not saying that you cannot make a correct program in Zig, but I prefer having checks that Rust compiler does, to not having them.