<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Low-Level on Gryt</title><link>https://gryt.io/tags/low-level/</link><description>Recent content in Low-Level on Gryt</description><generator>Hugo -- gohugo.io</generator><language>en</language><managingEditor>robin@gryt.io (Robin Penea)</managingEditor><webMaster>robin@gryt.io (Robin Penea)</webMaster><lastBuildDate>Thu, 12 Mar 2026 08:50:56 +0100</lastBuildDate><atom:link href="https://gryt.io/tags/low-level/index.xml" rel="self" type="application/rss+xml"/><item><title>SIMD implementation and Kotlin checkcast incompatibility</title><link>https://gryt.io/til/2026-03-12-simd-kotlin-checkcast-incompatibility/</link><pubDate>Thu, 12 Mar 2026 08:50:56 +0100</pubDate><author>robin@gryt.io (Robin Penea)</author><guid>https://gryt.io/til/2026-03-12-simd-kotlin-checkcast-incompatibility/</guid><description>&lt;p>I played around with SIMD in Kotlin inside a multi-threaded Mandelbrot generator. &lt;strong>My implementation was ~40% slower than the full scalar approach&lt;/strong>. What happened?&lt;/p></description><content>&lt;p>I played around with SIMD in Kotlin inside a multi-threaded Mandelbrot generator. &lt;strong>My implementation was ~40% slower than the full scalar approach&lt;/strong>. What happened?&lt;/p>
&lt;h2 id="simd">SIMD&lt;/h2>
&lt;p>If you don&amp;rsquo;t know &lt;a href="https://fr.wikipedia.org/wiki/Single_instruction_multiple_data">SIMD instructions&lt;/a>, here&amp;rsquo;s a &lt;em>very&lt;/em> short summary. Say you want to multiply 2 lists:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">a&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">b&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">7&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Multiply each element&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1*5, 2*6, 3*7, 4*8&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">res&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="n">i&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="nb">range&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">)):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">res&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">a&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">b&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">i&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>With SIMD, you can pack the 4 values into a wide CPU register, and multiply &lt;strong>with a single instruction&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">numpy&lt;/span> &lt;span class="k">as&lt;/span> &lt;span class="nn">np&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">a&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">np&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">array&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">4&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">dtype&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">np&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">float32&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">b&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">np&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">array&lt;/span>&lt;span class="p">([&lt;/span>&lt;span class="mi">5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">6&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">7&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">8&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="n">dtype&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">np&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">float32&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># SIMD here, the whole multiplication was done in a single instruction&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">a&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">b&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The idea is to execute the same &lt;em>operation&lt;/em> with variable &lt;em>operands&lt;/em>. Pure data parallelisation.&lt;/p>
&lt;h2 id="in-kotlin">In Kotlin&lt;/h2>
&lt;p>I experimented with the &lt;a href="https://openjdk.org/jeps/508">Vector API&lt;/a> inside a multi-threaded Mandelbrot generator. The perfect playground: the computation of each pixel is independent, and completely mathematical.&lt;/p>
&lt;p>After spending (a long) time converting my scalar code to SIMD, I benchmarked it. &lt;strong>Roughly 40% SLOWER 🤯&lt;/strong>. I dug around, and found this &lt;a href="https://netflixtechblog.com/optimizing-recommendation-systems-with-jdks-vector-api-30d2830401ec">Netflix blog post&lt;/a>. The short version is: &lt;strong>if you are in Kotlin, you should write your SIMD operations in Java&lt;/strong>.&lt;/p>
&lt;p>Why?&lt;/p>
&lt;p>If you write this in Kotlin:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-kotlin" data-lang="kotlin">&lt;span class="line">&lt;span class="cl">&lt;span class="k">val&lt;/span> &lt;span class="py">zx2&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="n">zx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">mul&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">zy&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The bytecode will look roughly like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">aload&lt;/span> &lt;span class="mi">7&lt;/span> &lt;span class="o">//&lt;/span> &lt;span class="nb">load&lt;/span> &lt;span class="n">zx&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">aload&lt;/span> &lt;span class="mi">8&lt;/span> &lt;span class="o">//&lt;/span> &lt;span class="nb">load&lt;/span> &lt;span class="n">zy&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">checkcast&lt;/span> &lt;span class="n">jdk&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">incubator&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">vector&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">Vector&lt;/span> &lt;span class="o">//&lt;/span> &lt;span class="n">CAST&lt;/span> &lt;span class="n">zy&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">Vector&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">invokevirtual&lt;/span> &lt;span class="n">DoubleVector&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mul&lt;/span> &lt;span class="o">//&lt;/span> &lt;span class="n">zx&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mul&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">zy&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The problem lies in &lt;code>checkcast&lt;/code>. The Vector API relies on JIT intrinsics to emit CPU vector instructions. Without intrinsification, it falls back to scalar execution. Kotlin is stricter about variance/generics than Java, and inserts a &lt;code>checkcast&lt;/code> instruction that prevents the JIT from applying those intrinsics. &lt;em>The JIT can no longer intrinsify each SIMD operation (&lt;code>sum&lt;/code>, &lt;code>mul&lt;/code>, &lt;code>blend&lt;/code>, etc)&lt;/em>.&lt;/p>
&lt;p>Compare it with the Java version:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="n">DoubleVector&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">zx2&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">zx&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">mul&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">zy&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And the bytecode:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">aload&lt;/span> &lt;span class="mi">7&lt;/span> &lt;span class="o">//&lt;/span> &lt;span class="nb">load&lt;/span> &lt;span class="n">zx&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">aload&lt;/span> &lt;span class="mi">8&lt;/span> &lt;span class="o">//&lt;/span> &lt;span class="nb">load&lt;/span> &lt;span class="n">zy&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">invokevirtual&lt;/span> &lt;span class="n">DoubleVector&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">mul&lt;/span> &lt;span class="o">//&lt;/span> &lt;span class="n">intrinsified&lt;/span> &lt;span class="n">to&lt;/span> &lt;span class="n">vmulpd&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Intrinsification&lt;/strong> means that the JIT compiler replaces the &lt;code>invokevirtual&lt;/code> by a direct CPU instruction. This is what we want for SIMD.&lt;/p>
&lt;h2 id="the-solution">The solution&lt;/h2>
&lt;p>I rewrote only the pure SIMD part of the algorithm in Java and benchmarked it again&amp;hellip; &lt;strong>4x faster than the scalar version 🤯🤯🤯&lt;/strong>.&lt;/p>
&lt;p>The performance gain will vary depending on your CPU and the JIT&amp;rsquo;s vectorization capabilities, but that was just amazing. What a ride!&lt;/p>
&lt;video controls width="100%">
&lt;source src="https://gryt.io/video/simd-kotlin-mandelbrot.webm" type="video/webm">
Your browser does not support the video tag.
&lt;/video></content></item></channel></rss>