# Surprising non-performance of String#split

**URL:** <https://forum.crystal-lang.org/t/surprising-non-performance-of-string-split/9113>\
**Category:** Help & Support\
**Created:** [August 25, 2026, 4:37pm UTC](https://forum.crystal-lang.org/t/surprising-non-performance-of-string-split/9113 "2026-08-25T16:37:08Z")\
**Posts on this page:** 17\
**Page:** 1

<div class="post-metadata">

**Author:** ![Xen](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/xen/32/2288_2.png) [@Xen](https://forum.crystal-lang.org/u/Xen)\
**Post date:** [August 25, 2026, 4:37pm UTC](https://forum.crystal-lang.org/t/surprising-non-performance-of-string-split/9113/1 "2026-08-25T16:37:08Z")

</div>

So I was hacking together a tool to chew through a CSV file and was considering performance as I’ll have to process something in the vicinity of 11 million rows. So I was playing around with a simple script to figure out how unique a column was. So, a nice-looking Crystal version:

```auto
Processed 1038785 rows in 2.5482900969999998 sec, 407640.00975513743 per sec

```

Then I let Gemini port it to PHP:

```auto
Processed 1038785 rows in 0.42527914047241 sec, 2442595.7004289 per sec

```

What the…? Crystal massively outpaced by an interpreted language?

Tried recompiling with `--release --production`:

```auto
Processed 1038785 rows in 0.68309683 sec, 1520699.488533712 per sec

```

Better, but still pretty pathetic next to PHP. Trying commenting out things, I found that the major time sink was `cols = line.split(SEPARATOR)`. I know, it allocates a new array of strings, and there’s more effective ways to do it when optimizing, but it’s up against `$cols = explode(self::SEPARATOR, rtrim($line, "\r\n"));` which does the same thing.

Is `String#split` really that slow? (I tried splitting both with a String and a Char separator).

---

<div class="post-metadata">

**Author:** ![kostya](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/kostya/32/3228_2.png) [@kostya](https://forum.crystal-lang.org/u/kostya)\
**Post date:** [August 25, 2026, 4:58pm UTC](https://forum.crystal-lang.org/t/surprising-non-performance-of-string-split/9113/2 "2026-08-25T16:58:56Z")

</div>

This is strange because, on [LangArena](https://kostya.github.io/LangArena/#/runtime_tab) benchmark `Etc::Words` is essentially only `@text.split(' ') { |w| frequencies.update(w, &.+(1)) }`, as you can see Crystal fastest in this test among 22 languages.

---

<div class="post-metadata">

**Author:** ![straight-shoota](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/straight-shoota/32/36_2.png) [@straight-shoota](https://forum.crystal-lang.org/u/straight-shoota)\
**Post date:** [August 25, 2026, 5:45pm UTC](https://forum.crystal-lang.org/t/surprising-non-performance-of-string-split/9113/3 "2026-08-25T17:45:35Z")

</div>

Interpreted vs. compiled isn’t really that relevant in this case. We’re talking about the performance of a built-in function of PHP which is implemented in C. And it’s probably implemented quite efficiently. Perhaps more efficiently than `String#split`.

It seems `String#split` misses a couple of optimizations. For example, when the separator is a single byte (or single-byte optimizable), which seems to apply to your use case?

There could also be some differences in the runtime, e.g. the efficiency of allocating strings. I presume there’s probably not much difference there, though. Or maybe PHP is at a bit of a disadvantage due to the interpreter overhead.

---

<div class="post-metadata">

**Author:** ![Xen](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/xen/32/2288_2.png) [@Xen](https://forum.crystal-lang.org/u/Xen)\
**Post date:** [August 25, 2026, 7:37pm UTC](https://forum.crystal-lang.org/t/surprising-non-performance-of-string-split/9113/4 "2026-08-25T19:37:42Z")

</div>

> [@kostya](#):
>
> This is strange because, on [LangArena](https://kostya.github.io/LangArena/#/runtime_tab) benchmark `Etc::Words` is essentially only `@text.split(' ') { |w| frequencies.update(w, &.+(1)) }`, as you can see Crystal fastest in this test among 22 languages.

Which would imply that PHP is faster than a number of compiled languages. Maybe the block version is faster?

> [@straight-shoota](#):
>
> Interpreted vs. compiled isn’t really that relevant in this case. We’re talking about the performance of a built-in function of PHP which is implemented in C.

You’re right, if I comment out the split and use hardcoded strings as a placeholder, things speeds up considerably.

```auto
# crystal run
Processed 1038786 rows in 0.323526817 sec, 3210818.842259991 per sec
# --release --production
Processed 1038786 rows in 0.069133719 sec, 15025750.314401574 per sec
# PHP
Processed 1038786 rows in 0.12817907333374 sec, 8104177.7958194 per sec

```

But PHP is still more than twice as fast than un-optimized Crystal.

It would seem that PHP is pretty well optimized for this case, tried having Gemini do a NodeJS version of the original script:

```auto
Processed 1038785 rows in 1.5042152469999999 sec, 690582.6822801777 per sec

```

Half the speed of PHP, but still faster than un-optimized Crystal.

> [@straight-shoota](#):
>
> It seems `String#split` misses a couple of optimizations. For example, when the separator is a single byte (or single-byte optimizable), which seems to apply to your use case?

Yeah, it’s just splitting on commas. The PHP version seems [awfully simple](https://github.com/php/php-src/blob/cc8187ad1b93b7196a42576f171719951852d04e/ext/standard/string.c#L805), but simple often helps hardware optimization.

---

<div class="post-metadata">

**Author:** ![ralsina](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/ralsina/32/2139_2.png) [@ralsina](https://forum.crystal-lang.org/u/ralsina)\
**Post date:** [August 25, 2026, 8:42pm UTC](https://forum.crystal-lang.org/t/surprising-non-performance-of-string-split/9113/5 "2026-08-25T20:42:02Z")

</div>

Can you provide a simple crystal file benchmarking the relevant fragments? Everyone loves an optimization thing

---

<div class="post-metadata">

**Author:** ![Xen](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/xen/32/2288_2.png) [@Xen](https://forum.crystal-lang.org/u/Xen)\
**Post date:** [August 25, 2026, 9:09pm UTC](https://forum.crystal-lang.org/t/surprising-non-performance-of-string-split/9113/6 "2026-08-25T21:09:55Z")

</div>

```crystal
# TODO: Write documentation for `Uniq`
module Uniq
  VERSION = "0.1.0"
  SEPARATOR = ','
  MAX_ITERATIONS = 1_500_000
  OUTPUT_EVERY = 100_000

  def self.main(args : Array(String))
    file = args[0]?

    abort("please provide a file") unless file

    isbns = Set(String).new

    processed : Int32 = 0
    start_time = Time.instant
    File.open(file, "r").each_line do |line|
      break if processed >= MAX_ITERATIONS
      cols = line.split(SEPARATOR)

      next if cols[0] == "ISBN"
      processed += 1

      isbns << cols[0]

      put_status(start_time, processed) if processed % OUTPUT_EVERY == 0
    end
    puts "Done"
    puts ""

    puts "#{processed} ISBN numbers, #{isbns.size} unique"

    put_status(start_time, processed)
  end

  def self.put_status(start_time : Time::Instant, processed : Int32)
    end_time = Time.instant
    seconds = end_time.duration_since(start_time).total_seconds
    rate = processed / seconds
    puts "Processed #{processed} rows in #{seconds} sec, #{rate} per sec"
  end

end

Uniq.main(ARGV)

```

The CSV has 7 columns, but the script only cares about the first. I can’t share the CSV I’ve used, the 165MiB size being one factor, but it shouldn’t be to hard to generate something.

---

<div class="post-metadata">

**Author:** ![HertzDevil](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/hertzdevil/32/1023_2.png) [@HertzDevil](https://forum.crystal-lang.org/u/HertzDevil)\
**Post date:** [August 26, 2026, 12:06am UTC](https://forum.crystal-lang.org/t/surprising-non-performance-of-string-split/9113/7 "2026-08-26T00:06:57Z")

</div>

The gist of the PHP implementation is the [`zend_memnstr`](https://github.com/php/php-src/blob/846773a4e9bfc6df7e672737e3b7b07c4d57b61f/Zend/zend_operators.h#L189) function which performs the actual substring search. There is:

- `memchr` for single-byte separators (there is no distinct code path for character separators versus strings)
- `memchr` + `memcmp` for subject strings up to 1023 bytes and separator strings up to 8 bytes
- [Sunday algorithm](https://hwlang.de/algorithmen/pattern/sundayen.htm) for longer subjects or separators

Things would be more complicated on our end if either the subject or the separator contains invalid UTF-8 byte sequences.

Have you tried something like `scan(/[^,]+/)` or `scan(/(.+?)(,|\z)/)` as well?

---

<div class="post-metadata">

**Author:** ![straight-shoota](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/straight-shoota/32/36_2.png) [@straight-shoota](https://forum.crystal-lang.org/u/straight-shoota)\
**Post date:** [August 26, 2026, 7:32am UTC](https://forum.crystal-lang.org/t/surprising-non-performance-of-string-split/9113/8 "2026-08-26T07:32:51Z")

</div>

No wonder that `memchr` is faster than iterating through the entire string with `Char::Reader`.

---

<div class="post-metadata">

**Author:** ![Xen](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/xen/32/2288_2.png) [@Xen](https://forum.crystal-lang.org/u/Xen)\
**Post date:** [August 26, 2026, 7:25pm UTC](https://forum.crystal-lang.org/t/surprising-non-performance-of-string-split/9113/9 "2026-08-26T19:25:26Z")

</div>

> [@HertzDevil](#):
>
> Have you tried something like `scan(/[^,]+/)` or `scan(/(.+?)(,|\z)/)` as well?

Even worse (without `--release`, comparable to the very first

```auto
Processed 1038785 rows in 4.328300535 sec, 239998.3530718483 per sec
Processed 1038785 rows in 4.887059216 sec, 212558.30021438398 per sec

```

> [@straight-shoota](#):
>
> No wonder that `memchr` is faster than iterating through the entire string with `Char::Reader`.

So, theoretically speaking, what would one do if one had the need to really paint go-faster-stripes on an app? Just call `memchr`?

---

<div class="post-metadata">

**Author:** ![jgaskins](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/jgaskins/32/2449_2.png) [@jgaskins](https://forum.crystal-lang.org/u/jgaskins)\
**Post date:** [August 26, 2026, 11:31pm UTC](https://forum.crystal-lang.org/t/surprising-non-performance-of-string-split/9113/10 "2026-08-26T23:31:43Z")

</div>

> [@Xen](#):
>
> without `--release`

There’s no useful performance data without `--release`, unfortunately. Compile-time optimizations are where the majority of a program’s performance comes from. The PHP interpreter you’re using almost certainly has them enabled.

---

<div class="post-metadata">

**Author:** ![Xen](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/xen/32/2288_2.png) [@Xen](https://forum.crystal-lang.org/u/Xen)\
**Post date:** [August 27, 2026, 6:04am UTC](https://forum.crystal-lang.org/t/surprising-non-performance-of-string-split/9113/11 "2026-08-27T06:04:53Z")

</div>

Well, even with `--release` the ratios between the different implementations is pretty much the same.

---

<div class="post-metadata">

**Author:** ![ysbaddaden](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/ysbaddaden/32/2252_2.png) [@ysbaddaden](https://forum.crystal-lang.org/u/ysbaddaden)\
**Post date:** [August 27, 2026, 8:58am UTC](https://forum.crystal-lang.org/t/surprising-non-performance-of-string-split/9113/12 "2026-08-27T08:58:52Z")

</div>

_Benchmarking is tricky._

An unoptimized Crystal program can’t be faster than an interpreted language when the latter spends most of its time in _optimized compiled code_.

Are you comparing `String#split` against `mb_str_split()` that are both Unicode aware, or to `str_split()` that expects ASCII? The latter can be much more optimized.

---

<div class="post-metadata">

**Author:** ![ralsina](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/ralsina/32/2139_2.png) [@ralsina](https://forum.crystal-lang.org/u/ralsina)\
**Post date:** [August 27, 2026, 11:10am UTC](https://forum.crystal-lang.org/t/surprising-non-performance-of-string-split/9113/13 "2026-08-27T11:10:53Z")

</div>

IIUC and you are extracting the 1st field which is a ISBN then you can use ascii even if the rest is not ascii :-)

---

<div class="post-metadata">

**Author:** ![straight-shoota](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/straight-shoota/32/36_2.png) [@straight-shoota](https://forum.crystal-lang.org/u/straight-shoota)\
**Post date:** [August 27, 2026, 12:22pm UTC](https://forum.crystal-lang.org/t/surprising-non-performance-of-string-split/9113/14 "2026-08-27T12:22:21Z")

</div>

The comparison is with [`explode`](https://www.php.net/manual/en/function.explode.php), not [`str_split`](https://www.php.net/manual/en/function.str-split.php). It seems `explode` is also Unicode aware. But it really doesn’t matter because the separator is a single ASCII character and it can be implemented with `memchr` (which the PHP engine does, `String#split` does not).

`memchr` typically utilizes SIMD instructions so it’s much more efficient than iterating each byte. We could easily do that in `String#split`, too.

---

<div class="post-metadata">

**Author:** ![HertzDevil](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/hertzdevil/32/1023_2.png) [@HertzDevil](https://forum.crystal-lang.org/u/HertzDevil)\
**Post date:** [August 27, 2026, 2:59pm UTC](https://forum.crystal-lang.org/t/surprising-non-performance-of-string-split/9113/15 "2026-08-27T14:59:55Z")

</div>

I implemented a [byte-based loop for separators that are ASCII characters](https://github.com/crystal-lang/crystal/pull/17309). It seems promising except when the splitted parts are very short (M \< 10). I also tried to use the single-pass Rabin-Karp algorithm from [this related PR](https://github.com/crystal-lang/crystal/pull/17305) for multibyte separators, and the results are _slower_ than the existing naive loop for some reason.

However:

> [@Xen](#):
>
> The CSV has 7 columns, but the script only cares about the first.

If this is the case then `#partition` or even `#index` is enough, and the Crystal code will probably be on par with a PHP equivalent.

---

<div class="post-metadata">

**Author:** ![jgaskins](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/jgaskins/32/2449_2.png) [@jgaskins](https://forum.crystal-lang.org/u/jgaskins)\
**Post date:** [August 28, 2026, 2:09am UTC](https://forum.crystal-lang.org/t/surprising-non-performance-of-string-split/9113/16 "2026-08-28T02:09:49Z")

</div>

> [@HertzDevil](#):
>
> I implemented a [byte-based loop for separators that are ASCII characters](https://github.com/crystal-lang/crystal/pull/17309). It seems promising except when the splitted parts are very short (M \< 10). I also tried to use the single-pass Rabin-Karp algorithm from [this related PR](https://github.com/crystal-lang/crystal/pull/17305) for multibyte separators, and the results are _slower_ than the existing naive loop for some reason.

I tried something similar the other night after seeing this thread and had the same results. Byte-by-byte iteration, precise allocations (no `realloc`s), and somehow it was slower than stdlib `String#split` for the inputs I tried. That was really surprising.

One thing that was a little over 3x as fast in the 1000x1000 case, though, was combining the precise allocations from that idea with a SWAR implementation based on ideas I got from [this blog post](https://byroot.github.io/ruby/performance/2026/08/13/speeding-up-ruby-hashes.html#swar-search) along with a bitmask to find occurrences of the separator.

> **Benchmark results**
>
> ```auto
> 1x1
> stdlib 50.27M ( 19.89ns) (± 1.81%) 80.0B/op 1.41× slower
> SWAR 70.86M ( 14.11ns) (± 1.41%) 48.0B/op fastest
> 
> 1x3
> stdlib 44.33M ( 22.56ns) (± 1.12%) 96.0B/op 1.50× slower
> SWAR 66.28M ( 15.09ns) (± 1.27%) 48.0B/op fastest
> 
> 1x10
> stdlib 37.98M ( 26.33ns) (± 0.89%) 96.0B/op 1.65× slower
> SWAR 62.73M ( 15.94ns) (± 1.12%) 48.0B/op fastest
> 
> 1x30
> stdlib 25.75M ( 38.84ns) (± 0.83%) 112B/op 2.02× slower
> SWAR 52.01M ( 19.23ns) (± 1.58%) 48.0B/op fastest
> 
> 1x100
> stdlib 12.04M ( 83.02ns) (± 2.36%) 192B/op 3.19× slower
> SWAR 38.37M ( 26.06ns) (± 0.83%) 48.0B/op fastest
> 
> 1x300
> stdlib 5.15M (194.32ns) (± 2.34%) 384B/op 4.39× slower
> SWAR 22.58M ( 44.30ns) (± 0.45%) 48.0B/op fastest
> 
> 1x1000
> stdlib 1.70M (589.11ns) (± 3.18%) 1.06kB/op 5.06× slower
> SWAR 8.58M (116.52ns) (± 0.88%) 48.0B/op fastest
> 
> 3x1
> stdlib 29.95M ( 33.39ns) (± 0.71%) 112B/op fastest
> SWAR 26.41M ( 37.86ns) (± 0.88%) 112B/op 1.13× slower
> 
> 3x3
> stdlib 24.10M ( 41.50ns) (± 0.80%) 160B/op fastest
> SWAR 23.13M ( 43.22ns) (± 0.53%) 160B/op 1.04× slower
> 
> 3x10
> stdlib 18.69M ( 53.51ns) (± 1.83%) 160B/op 1.20× slower
> SWAR 22.49M ( 44.47ns) (± 0.76%) 160B/op fastest
> 
> 3x30
> stdlib 11.06M ( 90.44ns) (± 0.77%) 208B/op 1.54× slower
> SWAR 17.00M ( 58.82ns) (± 0.78%) 208B/op fastest
> 
> 3x100
> stdlib 4.35M (230.03ns) (± 1.58%) 448B/op 2.60× slower
> SWAR 11.29M ( 88.56ns) (± 0.60%) 448B/op fastest
> 
> 3x300
> stdlib 1.78M (560.87ns) (± 1.89%) 1.0kB/op 2.90× slower
> SWAR 5.16M (193.66ns) (± 0.83%) 1.0kB/op fastest
> 
> 3x1000
> stdlib 559.52k ( 1.79µs) (± 2.99%) 3.07kB/op 2.94× slower
> SWAR 1.65M (607.06ns) (± 1.13%) 3.07kB/op fastest
> 
> 10x1
> stdlib 9.22M (108.46ns) (± 0.74%) 400B/op 1.14× slower
> SWAR 10.55M ( 94.77ns) (± 0.47%) 288B/op fastest
> 
> 10x3
> stdlib 7.51M (133.23ns) (± 0.70%) 560B/op 1.11× slower
> SWAR 8.30M (120.55ns) (± 0.72%) 448B/op fastest
> 
> 10x10
> stdlib 5.46M (183.16ns) (± 1.39%) 560B/op 1.47× slower
> SWAR 8.00M (124.97ns) (± 0.84%) 448B/op fastest
> 
> 10x30
> stdlib 3.13M (319.33ns) (± 1.12%) 720B/op 1.90× slower
> SWAR 5.96M (167.89ns) (± 1.03%) 608B/op fastest
> 
> 10x100
> stdlib 1.26M (792.86ns) (± 2.40%) 1.48kB/op 2.83× slower
> SWAR 3.56M (280.64ns) (± 0.59%) 1.38kB/op fastest
> 
> 10x300
> stdlib 511.23k ( 1.96µs) (± 2.50%) 3.36kB/op 2.96× slower
> SWAR 1.52M (659.85ns) (± 1.20%) 3.25kB/op fastest
> 
> 10x1000
> stdlib 166.05k ( 6.02µs) (± 2.81%) 10.3kB/op 2.97× slower
> SWAR 493.94k ( 2.02µs) (± 1.26%) 10.2kB/op fastest
> 
> 30x1
> stdlib 3.33M (299.95ns) (± 1.19%) 1.35kB/op 1.14× slower
> SWAR 3.80M (262.93ns) (± 0.97%) 768B/op fastest
> 
> 30x3
> stdlib 2.60M (384.11ns) (± 1.06%) 1.82kB/op 1.19× slower
> SWAR 3.09M (323.62ns) (± 1.09%) 1.22kB/op fastest
> 
> 30x10
> stdlib 1.89M (529.27ns) (± 1.37%) 1.82kB/op 1.53× slower
> SWAR 2.90M (345.21ns) (± 0.78%) 1.22kB/op fastest
> 
> 30x30
> stdlib 1.11M (898.79ns) (± 0.81%) 2.29kB/op 1.88× slower
> SWAR 2.09M (477.42ns) (± 0.72%) 1.69kB/op fastest
> 
> 30x100
> stdlib 431.38k ( 2.32µs) (± 2.40%) 4.63kB/op 2.78× slower
> SWAR 1.20M (834.17ns) (± 0.70%) 4.03kB/op fastest
> 
> 30x300
> stdlib 170.82k ( 5.85µs) (± 3.45%) 10.3kB/op 2.95× slower
> SWAR 503.63k ( 1.99µs) (± 0.53%) 9.66kB/op fastest
> 
> 30x1000
> stdlib 57.45k ( 17.41µs) (± 3.59%) 30.9kB/op 3.08× slower
> SWAR 176.96k ( 5.65µs) (± 0.48%) 30.3kB/op fastest
> 
> 100x1
> stdlib 1.16M (861.90ns) (± 0.58%) 5.21kB/op 1.06× slower
> SWAR 1.22M (816.49ns) (± 0.80%) 2.6kB/op fastest
> 
> 100x3
> stdlib 902.04k ( 1.11µs) (± 0.66%) 6.78kB/op 1.12× slower
> SWAR 1.01M (991.19ns) (± 1.09%) 4.16kB/op fastest
> 
> 100x10
> stdlib 649.83k ( 1.54µs) (± 1.33%) 6.78kB/op 1.39× slower
> SWAR 903.73k ( 1.11µs) (± 0.78%) 4.16kB/op fastest
> 
> 100x30
> stdlib 357.96k ( 2.79µs) (± 1.08%) 8.34kB/op 1.82× slower
> SWAR 651.39k ( 1.54µs) (± 1.05%) 5.72kB/op fastest
> 
> 100x100
> stdlib 135.92k ( 7.36µs) (± 2.86%) 16.1kB/op 2.85× slower
> SWAR 386.93k ( 2.58µs) (± 0.77%) 13.6kB/op fastest
> 
> 100x300
> stdlib 52.60k ( 19.01µs) (± 3.30%) 34.9kB/op 3.08× slower
> SWAR 162.17k ( 6.17µs) (± 0.87%) 32.3kB/op fastest
> 
> 100x1000
> stdlib 17.52k ( 57.07µs) (± 3.57%) 104kB/op 3.11× slower
> SWAR 54.47k ( 18.36µs) (± 0.92%) 101kB/op fastest
> 
> 300x1
> stdlib 456.22k ( 2.19µs) (± 0.49%) 11.3kB/op fastest
> SWAR 415.00k ( 2.41µs) (± 0.40%) 7.08kB/op 1.10× slower
> 
> 300x3
> stdlib 344.06k ( 2.91µs) (± 0.90%) 16.0kB/op fastest
> SWAR 340.31k ( 2.94µs) (± 0.66%) 11.7kB/op 1.01× slower
> 
> 300x10
> stdlib 240.64k ( 4.16µs) (± 0.68%) 16.0kB/op 1.26× slower
> SWAR 303.43k ( 3.30µs) (± 0.85%) 11.7kB/op fastest
> 
> 300x30
> stdlib 128.06k ( 7.81µs) (± 0.68%) 20.7kB/op 1.73× slower
> SWAR 222.07k ( 4.50µs) (± 0.41%) 16.4kB/op fastest
> 
> 300x100
> stdlib 46.43k ( 21.54µs) (± 3.09%) 44.1kB/op 2.85× slower
> SWAR 132.17k ( 7.57µs) (± 1.19%) 39.8kB/op fastest
> 
> 300x300
> stdlib 18.01k ( 55.52µs) (± 3.38%) 101kB/op 3.12× slower
> SWAR 56.15k ( 17.81µs) (± 0.90%) 96.1kB/op fastest
> 
> 300x1000
> stdlib 5.68k (175.99µs) (± 2.30%) 307kB/op 3.18× slower
> SWAR 18.09k ( 55.27µs) (± 0.50%) 303kB/op fastest
> 
> 1000x1
> stdlib 147.07k ( 6.80µs) (± 0.84%) 35.6kB/op fastest
> SWAR 132.05k ( 7.57µs) (± 0.59%) 23.4kB/op 1.11× slower
> 
> 1000x3
> stdlib 107.92k ( 9.27µs) (± 2.11%) 51.3kB/op fastest
> SWAR 104.95k ( 9.53µs) (± 0.68%) 39.1kB/op 1.03× slower
> 
> 1000x10
> stdlib 75.57k ( 13.23µs) (± 0.70%) 51.3kB/op 1.27× slower
> SWAR 95.67k ( 10.45µs) (± 0.95%) 39.1kB/op fastest
> 
> 1000x30
> stdlib 38.65k ( 25.88µs) (± 1.14%) 66.9kB/op 1.72× slower
> SWAR 66.44k ( 15.05µs) (± 1.38%) 54.7kB/op fastest
> 
> 1000x100
> stdlib 13.77k ( 72.64µs) (± 2.30%) 145kB/op 2.84× slower
> SWAR 39.12k ( 25.57µs) (± 0.87%) 133kB/op fastest
> 
> 1000x300
> stdlib 5.23k (191.25µs) (± 2.53%) 332kB/op 2.91× slower
> SWAR 15.20k ( 65.78µs) (± 0.68%) 320kB/op fastest
> 
> 1000x1000
> stdlib 1.75k (570.49µs) (± 4.06%) 0.99MB/op 3.23× slower
> SWAR 5.66k (176.77µs) (± 0.59%) 0.98MB/op fastest
> 
> ```

I can open it as yet another PR if that’s helpful.

---

<div class="post-metadata">

**Author:** ![HertzDevil](https://yyz2.discourse-cdn.com/flex036/user_avatar/forum.crystal-lang.org/hertzdevil/32/1023_2.png) [@HertzDevil](https://forum.crystal-lang.org/u/HertzDevil)\
**Post date:** [August 28, 2026, 9:45am UTC](https://forum.crystal-lang.org/t/surprising-non-performance-of-string-split/9113/17 "2026-08-28T09:45:12Z")

</div>

Some more implementations in other languages:

- [Ruby](https://github.com/ruby/ruby/blob/69b49ac7aedc5370512c158d6bbc7f077a85acd7/re.c#L285) (called [here](https://github.com/ruby/ruby/blob/69b49ac7aedc5370512c158d6bbc7f077a85acd7/string.c#L9901)): `memchr` for single bytes, [rolling hash or `memmem`](https://github.com/ruby/ruby/blob/69b49ac7aedc5370512c158d6bbc7f077a85acd7/re.c#L153) for separators up to `sizeof(size_t)` bytes, [Sunday](https://github.com/ruby/ruby/blob/69b49ac7aedc5370512c158d6bbc7f077a85acd7/re.c#L239) otherwise
- [Python](https://github.com/python/cpython/blob/24e5a55ccb0c5db68136d29f4c16c0bc21407db2/Objects/stringlib/fastsearch.h#L773) (called [here](https://github.com/python/cpython/blob/24e5a55ccb0c5db68136d29f4c16c0bc21407db2/Objects/stringlib/split.h#L160)): `memchr` for single bytes but plain loop for subjects shoter than `MEMCHR_CUT_OFF` bytes, [modified Boyer-Moore / Horspool](https://github.com/python/cpython/blob/24e5a55ccb0c5db68136d29f4c16c0bc21407db2/Objects/stringlib/fastsearch.h#L557) for short subjects or separators, [two-way algorithm](https://github.com/python/cpython/blob/24e5a55ccb0c5db68136d29f4c16c0bc21407db2/Objects/stringlib/fastsearch.h#L370) for long matches, and an adaptive algorithm between the two when the separator is longer than 33% of the subject
- [Rust](https://doc.rust-lang.org/src/core/str/pattern.rs.html) (called [here](https://doc.rust-lang.org/src/core/str/mod.rs.html#1670)): [`memchr`](https://doc.rust-lang.org/src/core/str/pattern.rs.html#441) for characters including multibyte ones, [two-way algorithm](https://doc.rust-lang.org/src/core/str/pattern.rs.html#1305) for strings
- [Go](https://github.com/golang/go/blob/2c7cbba805a3a9c7a9363f5ba72da59104f29fb3/src/internal/stringslite/strings.go#L28) (called [here](https://github.com/golang/go/blob/2c7cbba805a3a9c7a9363f5ba72da59104f29fb3/src/strings/strings.go#L342)): custom `memchr` for single bytes, custom assembly for very short matches, adaptive fallback to Rabin-Karp for long matches
