Saturday, 19 September 2026

[3 of 3] I benchmarked five Scala libraries. Which code would I rather read?

I started with synthetic benchmarks in part 1. Adding socket I/O in part 2 sent me into Kyo's scheduler. Along the way, Ox and Gears caught my attention. Their TCP throughput was close to cats-effect, they allocated much less, and I found their direct-style code easier to read.

For this last post, I added plain Loom and checked whether lower allocation also meant less memory. The results still don't give me one choice that wins everywhere.

Five runtimes, different answers

These are selected results from the new run, in operations per second. Higher is better. The ± values are JMH's 99.9% confidence-interval half-widths; compare libraries within each column.

Throughput, including runtime entry and task management
Runtime8 workersSpawn/joinBlocking TCPCallback TCP
cats-effect3,060 ± 921,894 ± 85145.10 ± 9.03141.53 ± 3.07
Kyo1,399 ± 265,355 ± 1044.95 ± 0.03139.99 ± 1.73
Loom / JDK3,420 ± 71558 ± 43143.49 ± 5.65137.69 ± 3.44
Ox3,580 ± 230494 ± 51140.29 ± 6.62136.78 ± 3.48
Gears3,214 ± 50377 ± 26140.28 ± 9.81139.25 ± 2.57

The eight-worker case processes 4,096 values, with 64 arithmetic rounds per value. Spawn/join creates a child, waits for it to finish, and repeats 1,000 times. Each TCP operation completes 256 exchanges over 64 persistent loopback connections. A separate server requests a 1 ms delay per response.

Kyo leads the tiny-task spawn/join case, while the callback TCP intervals overlap across all five. The slow Kyo blocking result uses defaults. In part 2, flushing and worker tuning brought it close to CE. That tuned result belongs to the earlier experiment.

All five ran every case shown here. The Loom row uses JDK virtual threads and primitives. The suspended effect-chain tests from part 1 have no equivalent Loom, Ox or Gears benchmark: replacing the chain with a direct loop changes what we measure. I haven't compared cancellation or resource-safety guarantees.

Less allocation didn't answer the memory question

I kept 100,000 tasks waiting, each holding a 1 KiB payload, and measured live heap after full GC and macOS process footprint. I also sampled memory during blocking TCP work at 64 connections. The numbers are medians from three fresh JVMs per case, in MiB. TCP measurements cover only the client.

Memory consumption: lower is better
RuntimeWaiting tasks: live heapWaiting tasks: process footprintActive TCP: process footprint
cats-effect191.0418.5152.5
KyoNot measured
Loom / JDK228.7390.090.3
OxNot measured
GearsNot measured

CE used less live heap for these waiting tasks. Loom had the smaller process footprint, which includes memory beyond live heap objects. So far, the memory harness covers only CE and Loom. I haven't measured Kyo, Ox or Gears here; the blanks don't indicate missing library support.

Both studies ran on 19 September 2026 on one M3 Max with JDK 25.0.3 and Scala 3.8.4: CE 3.7.1, Kyo 1.0.0-RC6, Ox 1.0.6 and Gears 0.3.1. Throughput used three forks, three 1-second warmups and three 1-second measurements, with a fixed 2 GiB G1 heap. Memory used G1 with a 64 MiB initial and 2 GiB maximum heap. These are short runs of these particular implementations on one machine.

What I want to maintain

I still enjoy writing with cats-effect and Kyo. After moving into Tagless Final, though, I sometimes find myself reading through a lot of ceremony to get to the business logic. Making every service generic in F[_] is a choice I can reconsider while still using CE.

That makes me more interested in direct-style concurrency. I often find its control flow easier to follow, even though writing effects is more fun for me. The benchmark results give me reasons to consider both.

AI makes me reconsider what I value

AI tools handled most of my earlier effect-system migration. If I spend more time reviewing generated code and less time writing it, how much should my enjoyment of writing a particular style count?

That even puts Java back on the table for me. Boilerplate bothers me less when I'm not typing all of it, but I still have to read and maintain it. I could live with more verbose code if I found it easier to follow what runs, how it fails and who owns a resource.

Effects and types can help me review code too. A version with fewer combinators might still hide a cancellation or cleanup bug.

I don't have one library to recommend for every project. I'd choose around the workload, the guarantees I need and the integrations available. Among the options that fit, I'd give more weight to what my team can comfortably read and maintain, whoever wrote it.

Sunday, 13 September 2026

[2 of 3] Blocking I/O made Kyo 30x slower. Then I tried flush().

I spent a week giving Kyo a chance in a real service. AI coding tools handled most of the effect-system migration.

After the first synthetic comparison, I also wanted to see what happened when the programs had to wait for actual socket I/O. The numbers below come from a separate TCP benchmark.

I added Gears and Ox too. Same TCP exchanges, connection limits and ordered results. The nonblocking numbers were close. Blocking exposed a much bigger difference.

Kyo led cats-effect by about 12% for nonblocking I/O at concurrency 8. At 64, it trailed by about 3%. That is already too mixed for a blanket claim that Kyo is faster. The earlier synthetic wins still describe those particular operations.

For blocking I/O at concurrency 64, cats-effect completed about 149 batches per second. Default Kyo managed about 5.

Each batch contained 256 requests over 64 persistent connections. A separate server JVM requested a 1 ms sleep before each response. These measurements used cats-effect 3.7.1 and Kyo 1.0.0-RC6 on one Mac with JDK 25.0.3, recorded on 6 September 2026.

Blocking configurationBatches/s, higher is better
cats-effect default149.00 ± 2.93
Kyo default4.96 ± 0.02
Kyo with flush before every exchange81.86 ± 25.62
Kyo with flush and scheduler tuning144.89 ± 3.60

Three JVM forks per case. The ± values are JMH's 99.9% confidence-interval half-widths. Flush alone varied substantially; the tuned Kyo and default CE intervals overlap.

The source investigation pointed to queued children collecting on a few workers, slow adaptation to short blocking calls, and a placement scan that could miss idle workers.

Calling the scheduler's flush hook before each exchange redistributed queued work:

Sync.defer {
    kyo.scheduler.Scheduler.get.flush()
    io.blocking(lane, index)
}

That is the blocking branch from the benchmark. Flush made a huge difference, but reaching CE's range also required setting coreWorkers, minWorkers, maxWorkers and scheduleStride to 64. Those are experimental settings for this workload. No library code changed.

Flush does not move the blocking call to another thread or create more workers. It gives queued tasks another chance to run elsewhere. We called it before every exchange; we have not tested whether once per worker would be enough.

The manual step bothers me. Kyo supports blocking through Sync.defer, yet getting good performance here required knowing about a scheduler hook. CE handled the same workload well with IO.blocking and its default runtime.

Kyo's Finagle integration already calls flush inside its blocking hook. When that integration is enabled and the call goes through the hook, it performs the flush automatically. That does not cover every arbitrary blocking call, and it does not apply our worker tuning. We did not benchmark Finagle.

Putting flush in every Sync.defer would also affect cheap side effects. My reading is that this could add scheduling overhead; I have no maintainer confirmation of that design rationale. A dedicated blocking helper seems worth exploring.

Gears and Ox landed around 139 blocking batches/s at concurrency 64 and allocated much less than CE. Their direct style is interesting even without a throughput win: ordinary loops, conditions and local helpers around I/O, with fewer effect combinators. That may matter more in everyday code than a small benchmark lead.

Kyo still has my attention. I also want to know how much scheduler knowledge an application will need.

Full tables, allocation, CPU and raw measurements, plus methods and reproduction. The suite passed 438 correctness checks. This is one loopback workload; it does not establish production performance or equivalent cancellation guarantees.

Saturday, 5 September 2026

[1 of 3 ]Kyo vs cats-effect: promising numbers and one big surprise

Kyo vs cats-effect: promising numbers and one big surprise

I find Kyo one of the most interesting projects in Scala right now. I get the feeling that some of the complexity we accept might not be necessary.

That feeling is easy to get excited about. I wanted to see some numbers too. This is the first post in a series about Kyo and cats-effect, starting with a small synthetic war.

I compared cats-effect 3.7.1 with Kyo 1.0.0-RC6, using fs2 3.13.0 on the cats-effect side for streaming.

Getting a fair comparison took more work than I expected. Two methods can look equivalent while doing different things with batches, buffers, or failures.

The suite passed 984 correctness checks. I ran JMH with three JVM forks and identical heap settings on one Mac with JDK 25.

Both sides use the same worker strategy and stream batch boundaries. The success-collection row uses parallel attempts and filtering, rather than native Async.gather. The stream rows do not directly compare parEvalMap with mapPar.

These are selected results with very little work per element. Ratios compare mean throughput; CE means cats-effect.

ConstructionHigher throughput
Uncontended permit acquisition/releaseKyo ~5.02×
Queue-backed stream batchesKyo ~3.70×
Sequential fiber spawn/joinKyo ~2.75×
Parallel stream batchesKyo ~1.93×
Integer queue, one producer and consumerKyo ~1.45×
Bounded workersKyo ~1.13×
Parallel attempt and success collectionConfidence intervals overlap
Left-associated suspended binds, depth 10,000CE ~1,895×

The permit, queue, and fiber results make Kyo worth a closer look. Around 5× the throughput for uncontended permits and 3.7× for queue-backed stream batches are encouraging numbers, even for these small workloads.

A little CPU work changed the results too. Bounded workers went from a small Kyo lead to CE ahead by about 2.22×. Kyo’s lead in parallel stream batches shrank to about 1.11×.

Then there’s the last row. CE handled the 10,000-step left-associated bind chain at about 1,895× Kyo’s throughput. Kyo allocated about 1.6 GB per complete chain, versus roughly 943 KB for CE. Making the chain ten times longer increased Kyo’s allocation about a hundredfold. That points to quadratic scaling in this case, and it needs a closer look.

These results make me more interested in trying Kyo. There’s enough here to justify a small application experiment, and a specific weakness to investigate along the way.

This is one machine and a set of small workloads. Cancellation, resource safety, and real I/O still need their own testing.

Source, full results, confidence intervals, and reproduction commands.

If you spot an unfair comparison, please point me to the code. I’d like to get it right.

Thursday, 28 February 2019

Aux pattern in 5 minutes

Every-time using everytime shapeless.Generic that returns shapeless.Generic.Aux
I was wondering why the construction
type Aux[T, Repr0] = Generic[T] { type Repr = Repr0 }
is needed?

If you are impassioned and want to save a time: AUX pattern is wrapping abstract type member into generic parametrisation to solve compiler limitations.

While the Scala's abstract type members are preferred to generic parametrization sometimes there are the problems with making the code compilable.
Lets revisit Type level programming for Streams and start from the case when Aux pattern isn’t needed:

import cats.Show

implicit val requestFunc = new Function[Int, String] {
  override def apply(in: Int): String = s"hello $in"
}
def process[Req, Resp](request: Req)(implicit func: Function[Req, Resp], s: Show[Resp]): String 
  = func(request).show

Our process function just applies the func to request and then using Show type class to represent the result value. Show and func are implicitly injected according to Req and Resp types.

Lets rewrite using abstract type member:

trait Request[Req] {
  type Resp
  def response: Resp
}

implicit def string2Sting = new Request[Int] {
  type Resp = String
  override def response: String = "Hello"
}

def process2[T](value: T)(implicit request: Request[T], m: Show[request.Resp]): String = 
 request.response.show

Method process2 isn’t compilable because we are trying parametrise Show with request.Response type:
error: illegal dependent method type: parameter may only be referenced in a subsequent parameter section

To fix this we introduce proxy type (Aux pattern).

type Aux[T2, B2] = Request[T2] {type B = B2}

And changing the method to:

def process3[Req, Resp](request: Req)(implicit aux: Aux[Req, Resp], m: Show[Resp]): String = 
aux.response.show

As a result we create Aux type that wraps abstract type member into generic to overcome Scala's compiler limitation!

Tuesday, 19 February 2019

Switch career to machine learning specialist

There is continues hype around AI. As this hype stream is still far from been over and predictively is going to grow in time it's natural for the software engineers to pry what is happening in the area of AI Engineer Jobs.

A lot of us been career switchers - got non IT education math or engineering and end up in IT industry as software engineers. Exceptional professions like 3D engine developers etc always have been demanding to heavy math background.

What is about ML - there are many courses/books like learn it in N days. Career switchers are used to get an experience driven into new background with consistent feeling lick of knowledge.

Does this skill help with diving into ML? My opinion it isn't - before starting ML courses it's mandatory to learn/refresh match background - here is graph for studying ML from scratch:




Tuesday, 11 December 2018

Hylomorphism in 1 minute

Hylomorphism is  Catamorphism compose to Anamorphism function.

It's presented as 

Hylomorphism = catamorphism(anamorphism(x))
 or
Hylomorphism = fold(unfold(x))

Basically it constructs (unfold) complex type (like trees, lists) and destructs (folds) back into representing value.

For example to get factorial from N we can 
a) unfold N to list of (n),(n-1),..,0
b) fold with prod function the list from previous step.

And possible Scala example implementing function for getting factorial from N with help of previously introduced list's Catamorphism and  Anamorphism is:

Anamorphism in 1 minute


An Anamorphism (from the Greek ἀνά "upwards" and μορφή "form, shape) over a type T is a function of type U → T that destructs the U in some way into a number of components. On the components that are of type U, it applies recursion to convert theme to T 's. After that it combines the recursive results with the other components to a T , by means of the constructor functions of T .

For example if we want to build List(N, N-1, …, 1) from N we would use anamorphism from Int to List[Int]. It’s the opposite operation to fold - unfold.