# Effectful is better at running unspecialized mtl-style code than transformers

**URL:** <https://discourse.haskell.org/t/effectful-is-better-at-running-unspecialized-mtl-style-code-than-transformers/14648>\
**Category:** Show and Tell\
**Created:** [September 4, 2026, 1:50am UTC](https://discourse.haskell.org/t/effectful-is-better-at-running-unspecialized-mtl-style-code-than-transformers/14648 "2026-09-04T01:50:04Z")\
**Posts on this page:** 1\
**Showing post:** 1

<div class="post-metadata">

**Author:** ![arybczak](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/arybczak/32/2527_2.png) [@arybczak](https://discourse.haskell.org/u/arybczak)\
**Post date:** [September 4, 2026, 1:50am UTC](https://discourse.haskell.org/t/effectful-is-better-at-running-unspecialized-mtl-style-code-than-transformers/14648/1 "2026-09-04T01:50:04Z")

</div>

This is something I’ve [discovered a year ago](https://github.com/haskell-effectful/effectful/pull/344) and wanted to include in a refined version of [benchmarks](https://github.com/haskell-effectful/effectful/blob/master/benchmarks/README.md), but since I didn’t manage to do that in such a long time, let me just share this particular bit.

A reminder:

1. **countdown** - a microbenchmark that effectively measures performance of monadic binds and the effect dispatch.
2. **filesize** - a more down to earth benchmark that does various things, including I/O.

Each benchmark has two flavours that affect the amount of effects available in the context:

- **shallow** - only effects necessary for the benchmark.

- **deep** - necessary effects + 5 redundant effects put into the context before and after the relevant ones (10 in total). This simulates a typical scenario in which the code uses only a portion of the total amount of effects available to the application.

Here are the results of `cabal run bench -- -p countdown.1000.mtl` and `cabal run bench -- -p filesize.1000.mtl` from the effectful repo on my machine (Ryzen 9950x3D) with GHC 9.10.3, neatly put in a table.

### Countdown

| Benchmark | Duration | Allocated | Copied by GC |
| --- | --- | --- | --- |
| effectful (shallow) | 35.8 μs ± 1.3 μs | 259 KB | 57 B |
| transformers (shallow) | 49.0 μs ± 4.5 μs | 407 KB | 53 B |
| **change** | **+36.9%** | **+57.1%** | **−7.0%** |
| effectful (deep) | 36.7 μs ± 2.7 μs | 266 KB | 190 B |
| transformers (deep) | 391 μs ± 34 μs | 3.0 MB | 603 B |
| **change** | **+965.1%** | **+1027.8%** | **+217.4%** |

### Filesize

| Benchmark | Duration | Allocated | Copied by GC |
| --- | --- | --- | --- |
| effectful (shallow) | 801 μs ± 25 μs | 6.5 MB | 192 KB |
| transformers (shallow) | 938 μs ± 90 μs | 7.0 MB | 267 KB |
| **change** | **+17.1%** | **+7.7%** | **+39.1%** |
| effectful (deep) | 805 μs ± 49 μs | 6.5 MB | 198 KB |
| transformers (deep) | 2.12 ms ± 172 μs | 14 MB | 694 KB |
| **change** | **+163.4%** | **+115.4%** | **+250.5%** |

> **Raw data**
>
> ```plaintext
> $ cabal run bench -- -p countdown.1000.mtl
> All
> countdown
> 1000
> mtl (effectful)
> shallow: OK
> 35.8 μs ± 1.3 μs, 259 KB allocated, 57 B copied, 6.0 MB peak memory
> deep: OK
> 36.7 μs ± 2.7 μs, 266 KB allocated, 190 B copied, 6.0 MB peak memory
> mtl (transformers)
> shallow: OK
> 49.0 μs ± 4.5 μs, 407 KB allocated, 53 B copied, 6.0 MB peak memory
> deep: OK
> 391 μs ± 34 μs, 3.0 MB allocated, 603 B copied, 6.0 MB peak memory
> 
> All 4 tests passed (0.87s)
> 
> ```
> 
> ```plaintext
> $ cabal run bench -- -p filesize.1000.mtl
> All
> filesize
> 1000
> mtl (effectful)
> shallow: OK
> 801 μs ± 25 μs, 6.5 MB allocated, 192 KB copied, 7.0 MB peak memory
> deep: OK
> 805 μs ± 49 μs, 6.5 MB allocated, 198 KB copied, 7.0 MB peak memory
> mtl (transformers)
> shallow: OK
> 938 μs ± 90 μs, 7.0 MB allocated, 267 KB copied, 7.0 MB peak memory
> deep: OK
> 2.12 ms ± 172 μs, 14 MB allocated, 694 KB copied, 7.0 MB peak memory
> 
> All 4 tests passed (0.90s)
> 
> ```

effectful always runs faster and allocates less, even in the shallow countdown microbenchmark which compares `Eff` vs `State` (which was pretty surprising to me). This is primarily because `State` is `StateT Identity`, so every monadic bind is 2 function calls vs 1 call with `Eff`. Basically, for a transformer stack of depth N, each monadic bind of an unspecialized mtl code is N function calls. That’s why results with a deep stack (not that deep really, easily reachable by a non-trivial application) are so bad.

Moreover, it’s not only about raw speed. Running mtl code via `Eff` also puts less pressure on the garbage collector.

Given this info, I’d like to announce that recently I’ve released effectful 2.7.0.0 and 2.7.1.0 with [a lot of goodies](https://hackage.haskell.org/package/effectful-2.7.1.0/changelog), so it’s never been a better time to try it out 😉

---

_[View the full topic](https://discourse.haskell.org/t/effectful-is-better-at-running-unspecialized-mtl-style-code-than-transformers/14648)._
