tl;dr summary: There may very well be reasons to refactor this code, but object allocation is not it.
As mentioned in the comments, it may be the case that these helpers actually have internal state that is specific to each run. If that is the case, then reusing the helper may lead to bugs.
When you analyze the lifetimes of objects in typical programs, you will notice two things:
- Almost all objects die young.
- Almost all of the survivors live forever.
And when you look at references, you will notice:
- Old objects almost never reference young objects.
This has been empirically measured across many different codebases in many different languages in many different domains.
The assumption that these three observations hold for every program is called the Generational Hypothesis. Practically all modern high-performance garbage collectors are optimized to take advantage of this. They do this by splitting the objects into two or more generations and using different strategies to collect these generations.
A typical way of doing this is with two generations. In the young generation, you simply keep a pointer to the start of the free space, and allocating an object is just incrementing that pointer by the size of the object. That's an extremely fast constant time operation, equivalent to allocating an object on the stack and faster than e.g. dynamic memory allocation with
malloc in C.
For collecting, you copy all the live objects into the old generation which is O(#live objects). If our Generational Hypothesis is correct, there should only be a small number of live objects, so this process should also be very fast. Note that copying the objects requires fixing up all existing references to those objects … but here is where the second aspect of our Generational Hypothesis comes in: we assume that there are almost no references from the old objects to the young objects, and as for the young objects, we need to touch those anyway to copy them, so we can fix up the references at the same time. And freeing the dead objects is literally just resetting the pointer.
For the old generation, in turn, we can then use a different algorithm which optimizes for the assumption that it won't actually have to collect many objects because most objects that have survived into the old generation will live forever. We can also collect the old generation much less often because again, we don't actually expect to be able to collect much anyway.
Cliff Click of Azul Systems gave a great example a couple of years ago, around 2008–2009: Cliff Click was running the simple Ant Colony Simulation that Rich Hickey used in his early Clojure talks on one of Azul's Azul Vega 3 Series 7300 Model 7380D Java Compute Accelerators with 864 cores and 768 GiBytes of RAM. (Those specs sounded a lot more impressive in 2008 when the machine came out.) The Ant Colony Simulation is designed to showcase Clojure's concurrency features and thus runs each Ant in a separate Agent. When running the simulation with many thousands of Ants and 700 threads in the thread pool, 700 CPU cores were fully loaded and the heap grew to 150 GiByte.
The simulation is written in a functional style and thus generates tons of temporary objects: it was generating more than GiByte/s of garbage! Yet the GC never even broke a sweat. The combined usage of the GC and the JIT compiler never even exceeded 50 CPU cores, and there were still well over 100 CPU cores left for other work.
If a GC in 2009 could handle 20 gigs of garbage per second, surely, a GC in 2021 can handle a couple of temporary helper objects!
As mentioned in the comments, the biggest killer for GC performance are objects that die middle-aged. Objects that live long enough to get copied out of the young generation (which is expensive) and then die in the old generation (which is again expensive).
The refactoring you are proposing would artificially extend the lifetime of those helper objects and thus break the assumption that "almost all objects die young".
Escape Analysis / Detection
Many modern high-performance language implementations also perform Escape Analysis, Escape Detection, or both. "Escape" here means "leaving the local scope", "Analysis" essentially means "statically", and "Detection" is the same but done dynamically at runtime. If Escape Analysis or Escape Detection can determine that no references to your object escape the local scope, it can actually allocate the object on the stack, and not even allocate it on the heap at all.
In the original version, it is very likely that the implementation will be able to determine that the anonymous instance of
ClassBHelper does not escape the scope of
processB. Whereas in your refactored code,
cbHelper is most definitely not local.
Garbage Collection, part 2
Another assumption that modern high-performance garbage collectors make is:
- Objects are only rarely mutated after they are initialized.
Mutation is one of the worst killers of GC performance. In fact, in the garbage collection community, they actually refer to the user program simply as the mutator because that is the most important aspect of it. Mutation is especially bad for concurrent collectors (collectors that run concurrently with the user program instead of stopping it) and also for parallel collectors (collectors that run multiple of their phases in an overlapping manner, or make use of parallelism within their phases, or both).
While this does not apply to your proposed refactoring, it is something that happens when you try to save object allocations by reusing objects but need to "reset" them in between before you use them.
By the way: if you think about it, you will realize that not having mutation also trivially implies the reference aspect of the Generational Hypothesis: if objects are not mutated after they are initialized, then they can't refer to newer objects that didn't exist yet when they were initialized!
An aside: functional programming
You will notice that functional programming actually satisfies the assumptions pretty well: there is obviously no mutation, thus also no references from old to young objects. And generally, functional programs are often designed as pipelines where each stage of the pipeline creates a slightly modified version of the object (note: "creates", not "changes it into"), thus creating a lot of short-lived objects but no long-lived ones.
In particular, generating all those short-lived temporary objects is often cited as a reason not to use FP for performance, but as we have seen, quite the opposite is true: this kind of usage pattern is exactly what modern high-performance GCs are optimized for!