Sunday, 13 September 2026

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

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.


Catamorphism in 2 minutes


Catamorphism (κατά "downwards" and μορφή "form, shape") on a type T is a function of type T → U that destruct and object of type T according to the structure of T, calls itself recursively of any components of T that are also of type T and combines this recursive result with the remaining components of T to a U.

And it's just an official name of fold/reduce on higher kinded types. 

For example getting the prod from list of integers is Catamorphism on the list:


Catamorphism in programming can be used to fold complex structures (lists, tress, sets)  into their representation via different type. As an example list catamorphism can be described as


And if we want to fold the list into prod from elements we would use: