<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://ostrzyciel.eu/feed.xml" rel="self" type="application/atom+xml"/><link href="https://ostrzyciel.eu/" rel="alternate" type="text/html" hreflang="en"/><updated>2026-05-15T13:38:23+00:00</updated><id>https://ostrzyciel.eu/feed.xml</id><title type="html">Piotr Sowiński</title><subtitle>Personal blog and website of Piotr Sowiński. I write about science, programming, cool tech, and more. </subtitle><entry><title type="html">Jelly and CBOR-LD: two flavors of binary RDF</title><link href="https://ostrzyciel.eu/blog/2025/jelly-and-cbor-ld/" rel="alternate" type="text/html" title="Jelly and CBOR-LD: two flavors of binary RDF"/><published>2025-04-01T00:00:00+00:00</published><updated>2025-04-01T00:00:00+00:00</updated><id>https://ostrzyciel.eu/blog/2025/jelly-and-cbor-ld</id><content type="html" xml:base="https://ostrzyciel.eu/blog/2025/jelly-and-cbor-ld/"><![CDATA[<p>I’ve recently learned about CBOR-LD (see <a href="https://docs.google.com/presentation/d/1ksh-gUdjJJwDpdleasvs9aRXEmeRvqhkVWqeitx5ZAE/">presentation</a>), a binary version of <a href="https://json-ld.org/">JSON-LD</a> with advanced compression mechanisms to make the serialized representation as small as possible. From what I understand, the main use case for CBOR-LD are QR codes and NFC tags, where reducing the size of data is crucial.</p> <p>This got me really interested, because I maintain <a href="https://w3id.org/jelly">Jelly</a>, also a binary RDF format. Jelly, however, focuses on very high read-write performance, and while it is pretty compact, I would not expect it to be comparable to CBOR-LD in terms of size.</p> <h2 id="how-does-cbor-ld-work">How does CBOR-LD work?</h2> <p>Quite honestly, I have a limited understanding of that, but here’s what I’ve figured out so far. In JSON-LD there is the notion of a “context”, which is basically a dictionary that maps JSON keys to more complex structures.</p> <p>Here is an example from the <a href="https://www.w3.org/TR/json-ld11/#example-7-in-line-context-definition">JSON-LD 1.1 spec</a>. We can take an input document like this:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[{</span><span class="w">
  </span><span class="nl">"http://schema.org/name"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="nl">"@value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Manu Sporny"</span><span class="p">}],</span><span class="w">
  </span><span class="nl">"http://schema.org/url"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nl">"@id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://manu.sporny.org/"</span><span class="w"> </span><span class="p">}],</span><span class="w">
  </span><span class="nl">"http://schema.org/image"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nl">"@id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://manu.sporny.org/images/manu.png"</span><span class="w"> </span><span class="p">}]</span><span class="w">
</span><span class="p">}]</span><span class="w">
</span></code></pre></div></div> <p>…add a context like this:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"@context"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://schema.org/name"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"image"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"@id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://schema.org/image"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"@type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"@id"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"homepage"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"@id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://schema.org/url"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"@type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"@id"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div> <p>…and we get a compacted document like this:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"@context"</span><span class="p">:</span><span class="w"> </span><span class="err">...</span><span class="w"> </span><span class="p">,</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Manu Sporny"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"homepage"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://manu.sporny.org/"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"image"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://manu.sporny.org/images/manu.png"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div> <p>I’ve replaced the context with <code class="language-plaintext highlighter-rouge">...</code> here, because there are actually two ways you can use a context in JSON-LD: you can either inline it (put the whole <code class="language-plaintext highlighter-rouge">@context</code> map there) or you can refer to it by a URL. The latter is a clever trick, because you are removing a bunch of information from the document and putting it somewhere else. The producer of the data and the consumer must agree on the context, but once they do, the data can be very compact. This behavior is in fact unique to JSON-LD among standardized RDF formats. The others (like Turtle) never put data outside of the document.</p> <p>CBOR-LD is first of all a translation of JSON-LD to binary, which already saves a few bytes. But it also takes this context mechanism to the extreme with what it calls <strong><em>semantic compression</em></strong>. The context dictionary is then indexed by integers (not strings), and you can also put more pattern types in the dictionary than with JSON-LD. CBOR-LD also uses value encodings of literals (so, instead of a string “1234” you would have an actual integer), which is another way to save space, while losing a bit of information (RDF purists might not like that).</p> <p>My explanation is probably not very accurate, so I recommend you check out the <a href="https://docs.google.com/presentation/d/1ksh-gUdjJJwDpdleasvs9aRXEmeRvqhkVWqeitx5ZAE/">presentation slides</a> I mentioned earlier or the <a href="https://json-ld.github.io/cbor-ld-spec/">draft spec</a> for more details.</p> <h2 id="how-does-jelly-work">How does Jelly work?</h2> <p>That’s really a story for a few other blogposts! 😆 But, in short, <a href="https://w3id.org/jelly">Jelly</a> is much closer to more traditional RDF formats (like <a href="https://www.w3.org/TR/turtle/">Turtle</a>) than CBOR-LD. Internally, it looks a lot like an N-Triples/N-Quads file (just a long list of triples), but the triples are interspersed with dictionary entries.</p> <p>For example, if we are going to use the IRI <code class="language-plaintext highlighter-rouge">http://xmlns.com/foaf/0.1/knows</code>, we put the prefix <code class="language-plaintext highlighter-rouge">http://xmlns.com/foaf/0.1/</code> in the prefix lookup under, say, ID 4, and then we put the string <code class="language-plaintext highlighter-rouge">knows</code> in the name lookup under ID 2. The IRI then looks like this: <code class="language-plaintext highlighter-rouge">RdfIri(4, 2)</code>. The lookups are updated as we go along through the stream, evicting old entries if needed. They are sent via the same byte stream as the triples/quads, so you get the entire data in one file – just like in Turtle or N-Triples.</p> <p>There are also a few other techniques used, including compressing repeating terms and some tricks with differential compression of lookup references… You can find the <a href="https://jelly-rdf.github.io/dev/specification/serialization/">full spec on Jelly’s website</a>, if you are interested. The nice part about it is that it’s very fast, works automatically with any RDF data (no need to think about contexts), can handle data of any size (with bounded memory!), and is sufficiently compact for most use cases.</p> <h2 id="experiment-1--iot-data">Experiment 1 – IoT data</h2> <p>Like I mentioned, the two formats have very different use cases. Being shameless and curious, I first tried them with something that Jelly was built for – streaming IoT data. I took the <a href="https://w3id.org/riverbench/datasets/assist-iot-weather-graphs/1.0.3"><code class="language-plaintext highlighter-rouge">assist-iot-weather-graphs</code></a> dataset from RiverBench. The dataset is a series of weather observations, with each measurement being a named RDF graph with a bunch of triples describing the sensors, wind speed, temperatures, and so on (you can find samples on <a href="https://w3id.org/riverbench/datasets/assist-iot-weather-graphs/1.0.3">its description page</a> in RiverBench).</p> <p>The code for the experiments is <strong><a href="https://github.com/Ostrzyciel/jelly-cbor-ld/blob/072f69920978f4e98c4ab3311c8135f0b27cd366/src/main/java/eu/ostrzyciel/experiments/jelly_cbor_ld/Main.java">here</a></strong>. It uses the <a href="https://w3id.org/jelly/jelly-jvm/2.9.x/user/titanium/">new shiny integration of Jelly-JVM with the Titanium RDF API</a>. The new <a href="https://github.com/filip26/titanium-rdf-api">Titanium API</a> is intended to be a minimalistic bridge between different RDF libraries and I think it does a great job at that – linking Jelly-JVM to Titanium JSON-LD was a breeze.</p> <p>I first needed to encode the data in JSON-LD, and then compact it. For that I needed a context. Now – I’m bad at JSON-LD, so I used the context creation algorithm that Apache Jena uses, which is to simply use all explicitly defined prefixes and build a context like this:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"@context"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"aiot"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://assist-iot.eu/ontologies/aiot#"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"aiot_p2"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://assist-iot.eu/ontologies/aiot_p2#"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"geosparql"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.opengis.net/ont/geosparql#"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"om"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.ontology-of-units-of-measure.org/resource/om-2/"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"pilot"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://assist-iot.eu/pilot2_rdf/"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"prov"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/ns/prov#"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"rdfs"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/2000/01/rdf-schema#"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"sosa"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/ns/sosa/"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ssn"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/ns/ssn/"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"xsd"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://www.w3.org/2001/XMLSchema#"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div> <p>Note that this is likely not even close to being optimal, but it is simple to build and analogous to Turtle. Also note that Jelly does not need you to manually specify the prefixes, it figures them out automatically from the raw data.</p> <p>I then put the context under a local URL (in a file on disk), so that the context is just an URL reference, and compressed it with CBOR-LD.</p> <p>Here are the results, in bytes:</p> <table class="table table-sm"> <thead> <tr> <th>Stream element</th> <th style="text-align: right">Jelly</th> <th style="text-align: right">JSON-LD</th> <th style="text-align: right">JSON-LD (compacted)</th> <th style="text-align: right">CBOR-LD</th> </tr> </thead> <tbody> <tr> <td>0</td> <td style="text-align: right">3929</td> <td style="text-align: right">13321</td> <td style="text-align: right">6735</td> <td style="text-align: right">5273</td> </tr> <tr> <td>1</td> <td style="text-align: right">2259</td> <td style="text-align: right">13321</td> <td style="text-align: right">6735</td> <td style="text-align: right">5273</td> </tr> <tr> <td>2</td> <td style="text-align: right">2261</td> <td style="text-align: right">13321</td> <td style="text-align: right">6735</td> <td style="text-align: right">5273</td> </tr> <tr> <td>3</td> <td style="text-align: right">2260</td> <td style="text-align: right">13320</td> <td style="text-align: right">6734</td> <td style="text-align: right">5272</td> </tr> </tbody> </table> <p>One thing I didn’t mention is that Jelly can work with RDF streams, so it is able to encode multiple RDF datasets (in this case, 1 dataset = 1 weather observation) as a single stream. This is why the first frame in Jelly’s stream is larger – it contains the prefixes and other lookup entries. The subsequent frames are much smaller, because they only contain the dictionary references and literals.</p> <p>CBOR-LD seems to be much worse here, but like I mentioned, I did a very bad job at preparing the JSON-LD context. If I used something really fine-tuned to the kind of data I’m working with, the results would be much better (probably better than Jelly!). If someone wants to try their hand at this, I would be very interested in updating the blogpost with them. I think it should be possible to get it down below 1000 bytes.</p> <h2 id="experiment-2--verifiable-credentials">Experiment 2 – Verifiable Credentials</h2> <p>Now, this is what CBOR-LD was really built for. <a href="https://w3c-ccg.github.io/vc-barcodes/">Verifiable Credentials (VCs)</a> contain information about, for example, your driver’s license or vaccination status. They are meant to be stored in QR/barcodes or NFC tags, so the smaller the better.</p> <p>The <a href="https://github.com/filip26/ld-cli?tab=readme-ov-file#custom-cbor-ld-dictionaries"><code class="language-plaintext highlighter-rouge">ld-cli</code> tool</a> has a great example of something like this – only 145 bytes for the whole thing! I wanted to see how Jelly would do with it.</p> <p>The code for this experiment is in <a href="https://github.com/Ostrzyciel/jelly-cbor-ld/blob/072f69920978f4e98c4ab3311c8135f0b27cd366/src/main/java/eu/ostrzyciel/experiments/jelly_cbor_ld/Main.java">the same file as before</a>.</p> <table class="table table-sm"> <thead> <tr> <th>Stream element</th> <th style="text-align: right">Jelly</th> <th style="text-align: right">JSON-LD (compacted)</th> <th style="text-align: right">CBOR-LD</th> </tr> </thead> <tbody> <tr> <td>0</td> <td style="text-align: right">1337</td> <td style="text-align: right">1098</td> <td style="text-align: right">145</td> </tr> <tr> <td>1</td> <td style="text-align: right">331</td> <td style="text-align: right">1098</td> <td style="text-align: right">145</td> </tr> <tr> <td>2</td> <td style="text-align: right">331</td> <td style="text-align: right">1098</td> <td style="text-align: right">145</td> </tr> </tbody> </table> <p>Remember, CBOR-LD is putting essentially the entire dictionary (IRIs and stuff) in the context, which is not part of the message. In Jelly, I ran the same VC through the encoder a few times, to get a similar effect as with the IoT data. The first frame is larger, because it contains the prefixes and dictionary entries, but the subsequent frames are much smaller.</p> <p>So – if you treated the first Jelly frame as a “context” of sorts that is pre-shared between the producer and the consumer, you would get a very similar mechanism as in CBOR-LD. In that case I’d say that the comparison between the <code class="language-plaintext highlighter-rouge">331</code> bytes of Jelly to the <code class="language-plaintext highlighter-rouge">145</code> bytes of CBOR-LD is a fair one.</p> <p>I also tried applying binary compression (gzip and zstd) to the outputs (<a href="https://github.com/Ostrzyciel/jelly-cbor-ld/blob/072f69920978f4e98c4ab3311c8135f0b27cd366/src/main/java/eu/ostrzyciel/experiments/jelly_cbor_ld/Compression.java">see the code</a>):</p> <table class="table table-sm"> <thead> <tr> <th>Binary compression</th> <th style="text-align: right">Jelly (el. 1)</th> <th style="text-align: right">JSON-LD (compacted)</th> <th style="text-align: right">CBOR-LD</th> </tr> </thead> <tbody> <tr> <td>None</td> <td style="text-align: right">331</td> <td style="text-align: right">1098</td> <td style="text-align: right">145</td> </tr> <tr> <td>gzip (level 6)</td> <td style="text-align: right">274</td> <td style="text-align: right">549</td> <td style="text-align: right">168</td> </tr> <tr> <td>zstd (level 19)</td> <td style="text-align: right">264</td> <td style="text-align: right">552</td> <td style="text-align: right">157</td> </tr> </tbody> </table> <p>Setting higher compression levels did not make the files smaller. We can see that you can shave off a few bytes from the Jelly file, but it’s still far from being as compact as CBOR-LD. On the other hand, the CBOR file gets a bit larger, because the data is already very tightly compressed.</p> <h2 id="concluding-thoughts">Concluding thoughts</h2> <p>Jelly and CBOR-LD are both binary RDF formats, but with very different goals, use cases, and strengths.</p> <p>CBOR-LD can be really, really compact, if you put in the work of defining a good context. Jelly does everything automatically for you (“one size fits all”), but is not as compact. There are <a href="https://github.com/Jelly-RDF/jelly-protobuf/issues/14">things that could be improved</a> in Jelly to make it a bit smaller (and I’m working on it!), but it often does require sacrificing some interoperability or speed.</p> <p>On the very far extreme end, you could define a binary format that has a context that’s a pattern of triples with a couple of blanks to fill in (like a template). The binary representation would be just the serialized field contents, in sequence. Literally zero overhead. Flexibility would be zero as well, but it would be the most compact format possible. At this point I start to wonder if it wouldn’t make sense to just use a dedicated binary format for the specific use case, and then link it to an <a href="https://rml.io/">RML template</a> or something like that. 😆 Quite honestly, I think that this kind of approach may make quite a lot sense for narrowly-defined messages – from what I understand this is similar to what the <a href="https://www.smart-edge.eu/">SmartEdge EU project</a> is working on.</p> <p>The point is that there is always a trade-off between compactness, speed, and flexibility. Jelly is optimized for speed and flexibility, while CBOR-LD is optimized for compactness. The choice of format depends on the use case. I think CBOR-LD is an excellent idea overall, but one with relatively narrow use cases. There is also the issue of how to keep track of the registered contexts, and the related security implications – this was brought up at the last W3C JSON-LD CG meeting. I will keep working on making Jelly a bit more compact, but there is only so much that you can do without sacrificing the other properties.</p> <p>Caveat: I did not compare speed here, at all. While Jelly is <a href="https://w3id.org/jelly/dev/performance/">insanely fast</a>, especially for IoT data, I would expect CBOR-LD to be way slower, because it’s another layer of complexity on an already complex format (JSON-LD). I guess that’s a topic for future blogpost.</p> <h2 id="shout-outs">Shout-outs</h2> <p>Many thanks to <a href="https://github.com/filip26/">filip26</a> for his help in explaining to me how CBOR-LD works and how to use it. He made the amazing <a href="https://github.com/filip26/titanium-rdf-api">Titanium RDF API</a> I used to convert between Jelly and CBOR-LD, the <a href="https://github.com/filip26/iridium-cbor-ld">iridium-cbor-ld library</a>, and the <a href="https://github.com/filip26/ld-cli"><code class="language-plaintext highlighter-rouge">ld-cli</code> tool</a>, which was super helpful.</p> <p>Also, shout out to the <a href="https://www.w3.org/groups/cg/json-ld/">W3C JSON-LD Community Group</a>, through which I got to know CBOR-LD better!</p>]]></content><author><name></name></author><category term="rdf"/><category term="jelly"/><category term="cbor-ld"/><category term="iot"/><category term="compression"/><summary type="html"><![CDATA[Playing with two binary RDF formats, Jelly and CBOR-LD, through experiments on IoT data and Verifiable Credentials.]]></summary></entry><entry><title type="html">Making Scala go fast: avoiding allocations</title><link href="https://ostrzyciel.eu/blog/2024/scala-performance-avoiding-allocation/" rel="alternate" type="text/html" title="Making Scala go fast: avoiding allocations"/><published>2024-09-08T00:00:00+00:00</published><updated>2024-09-08T00:00:00+00:00</updated><id>https://ostrzyciel.eu/blog/2024/scala-performance-avoiding-allocation</id><content type="html" xml:base="https://ostrzyciel.eu/blog/2024/scala-performance-avoiding-allocation/"><![CDATA[<p>I love Scala – it’s a very elegant and powerful language. This is why I decided to use it to write a high-performance serializer for knowledge graphs (<a href="https://w3id.org/jelly/jelly-jvm/dev/">Jelly-JVM</a>, go check it out if you are a fan of RDF) and… oh boy, was it an adventure. I spent <em>way</em> too much time optimizing the code to make the serializer as fast as possible, and in doing that I’ve learned a lot about the quirks of Scala 3 and JVM optimization, which I thought may be worth sharing with others.</p> <p><strong>Welcome to the first post in the “gotta go fast” series about Scala 3 and JVM performance.</strong> If you don’t want to miss future posts, consider subscribing to my <a href="/feed.xml">RSS/Atom feed</a>.</p> <p>In this post, we explore the ways in which you can avoid the overhead of allocating things on the heap, which for me turned out to be the place where I’ve achieved the most noticeable performance gains.</p> <hr/> <p>First things first – please remember that <em>premature optimization is the root of all evil</em>. Modern JVMs are pretty smart and can optimize a lot of things for you. <u>Do not</u> optimize your Scala code before you find a clear issue with its performance. <strong>Many techniques described here go against “the Scala way”, may make your code uglier, or make it easier to introduce bugs into your code.</strong> But, they make it go faster. 😉 You have been warned.</p> <p>With this disclaimer out of the way…</p> <h2 id="the-horror-of-heap-allocations">The horror of heap allocations</h2> <p>At some point you may run in to a situation where your application spends the majority of CPU time allocating objects in the heap and then freeing them with the garbage collector (GC). You can check this easily with any decent profiler. Sure, the allocation mechanism in modern JVMs is pretty efficient<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>, and there is a whole science dedicated to choosing the right GC for a given job, but an allocation is an allocation, and it must come with <em>some</em> performance cost. If you are for example allocating millions of objects per seconds, then it may be worth taking a closer look at.</p> <p>OK, so we have too many allocations – the solution then is obvious: <strong>allocate less!</strong> But how?</p> <h2 id="the-scala-bloat-option-either-try">The Scala “bloat”: <code class="language-plaintext highlighter-rouge">Option</code>, <code class="language-plaintext highlighter-rouge">Either</code>, <code class="language-plaintext highlighter-rouge">Try</code></h2> <p><code class="language-plaintext highlighter-rouge">Option[T]</code> was created as a functional and type-safe alternative to <code class="language-plaintext highlighter-rouge">null</code> in Java, which admittedly is one of the most annoying parts of the language. Same with <code class="language-plaintext highlighter-rouge">Either[T1, T2]</code> for unions and <code class="language-plaintext highlighter-rouge">Try[T]</code> for exceptions. Let’s look at an example:</p> <div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nn">scala.util.</span><span class="o">{</span><span class="nc">Success</span><span class="o">,</span> <span class="nc">Try</span><span class="o">}</span>

<span class="k">val</span> <span class="nv">cat</span><span class="k">:</span> <span class="kt">String</span> <span class="o">=</span> <span class="s">"Cat!"</span>
<span class="k">val</span> <span class="nv">someCat</span><span class="k">:</span> <span class="kt">Option</span><span class="o">[</span><span class="kt">String</span><span class="o">]</span> <span class="k">=</span> <span class="nc">Some</span><span class="o">(</span><span class="n">cat</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">rightCat</span><span class="k">:</span> <span class="kt">Either</span><span class="o">[</span><span class="kt">Unit</span>, <span class="kt">String</span><span class="o">]</span> <span class="k">=</span> <span class="nc">Right</span><span class="o">(</span><span class="n">cat</span><span class="o">)</span>
<span class="k">val</span> <span class="nv">successCat</span><span class="k">:</span> <span class="kt">Try</span><span class="o">[</span><span class="kt">String</span><span class="o">]</span> <span class="k">=</span> <span class="nc">Success</span><span class="o">(</span><span class="n">cat</span><span class="o">)</span>
</code></pre></div></div> <p>Seems pretty normal. But, this ordinary snippet contains four (!) allocations: one <code class="language-plaintext highlighter-rouge">String</code>, one <code class="language-plaintext highlighter-rouge">Some[String]</code>, one <code class="language-plaintext highlighter-rouge">Right[String]</code>, and one <code class="language-plaintext highlighter-rouge">Success[String]</code>. So, every time you simply want to say “yes, this variable really does have a value” and use <code class="language-plaintext highlighter-rouge">Some(value)</code>, you make an allocation. This can add up very, very quickly in a hot path.</p> <p>You can get around this by simply not using these features of Scala. I definitely <em>do not</em> recommend doing this for public APIs, only for internal code. Please also take care to test your code properly, because these great Scala features were designed to protect your code from bugs.</p> <ul> <li>Replace <code class="language-plaintext highlighter-rouge">Option[T]</code> with just <code class="language-plaintext highlighter-rouge">T</code>. Instead of <code class="language-plaintext highlighter-rouge">None</code> use <code class="language-plaintext highlighter-rouge">null</code>. Note that using a <code class="language-plaintext highlighter-rouge">None</code> by itself does not allocate anything new, as <code class="language-plaintext highlighter-rouge">None</code> is a Scala object (singleton), so you only save allocations on removing <code class="language-plaintext highlighter-rouge">Some(...)</code>.</li> <li>Replace <code class="language-plaintext highlighter-rouge">Either[T1, T2]</code> with a <a href="https://docs.scala-lang.org/scala3/book/types-union.html">union type</a>: <code class="language-plaintext highlighter-rouge">T1 | T2</code>. Scala supports these pretty well. The disadvantage is that instead of nice functional methods for accessing the right/left value of an Either, you must use <code class="language-plaintext highlighter-rouge">match</code> expressions instead. Internally, matching by type uses <code class="language-plaintext highlighter-rouge">isInstanceOf</code>, which is really fast on modern JVMs. On the other hand, union types may be seen as <em>nicer</em> in some use cases, as they support the type algebra.</li> <li>Replace <code class="language-plaintext highlighter-rouge">Try[T]</code> with good ol’ exceptions… or something else entirely, like a union type. You should not rely on exceptions for the “normal” control flow of your program, but if what you are catching really are <em>exceptions</em>, then it’s probably fine.</li> </ul> <p>This will make your code look more like Java than Scala – it’s up to you to decide if the speedup is worth it.</p> <h2 id="pre-allocation">Pre-allocation</h2> <p>I’ve already mentioned that using <code class="language-plaintext highlighter-rouge">None</code> by itself is free in terms of allocations – it is allocated as a singleton, so every time you use it, you get a reference to the exact same object on the heap. You can use this trick in a few other situations as well.</p> <h3 id="default-instance">Default instance</h3> <p>Let’s take a simple example:</p> <div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="k">class</span> <span class="nc">Cat</span><span class="o">(</span><span class="n">friends</span><span class="k">:</span> <span class="kt">List</span><span class="o">[</span><span class="kt">Cat</span><span class="o">]</span> <span class="k">=</span> <span class="nc">List</span><span class="o">())</span>

<span class="k">def</span> <span class="nf">getLonelyCat</span><span class="k">:</span> <span class="kt">Cat</span> <span class="o">=</span> <span class="nc">Cat</span><span class="o">()</span>
</code></pre></div></div> <p>Each time we call the <code class="language-plaintext highlighter-rouge">getLonelyCat</code> method, a new <code class="language-plaintext highlighter-rouge">Cat</code> instance is allocated on the heap. However, as the class is immutable, this does not benefit us in any way. We could have 50 different callers obtain a new lonely cat in this way and none of them would be able to modify their instance of the <code class="language-plaintext highlighter-rouge">Cat</code>. Why not instead give them the exact same instance, saving 49 allocations? They won’t realize the difference, after all…</p> <div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">object</span> <span class="nc">Cat</span><span class="k">:</span>
  <span class="kt">val</span> <span class="kt">defaultInstance:</span> <span class="kt">Cat</span> <span class="o">=</span> <span class="nc">Cat</span><span class="o">()</span>

<span class="k">case</span> <span class="k">class</span> <span class="nc">Cat</span><span class="o">(</span><span class="n">friends</span><span class="k">:</span> <span class="kt">List</span><span class="o">[</span><span class="kt">Cat</span><span class="o">]</span> <span class="k">=</span> <span class="nc">List</span><span class="o">())</span>

<span class="k">def</span> <span class="nf">getLonelyCat</span><span class="k">:</span> <span class="kt">Cat</span> <span class="o">=</span> <span class="nv">Cat</span><span class="o">.</span><span class="py">defaultInstance</span>
</code></pre></div></div> <p>Here we have created a companion object to the <code class="language-plaintext highlighter-rouge">Cat</code> class, which allocates a single new lonely cat at application startup (in Java lingo: <em>static initializer block</em>). We do not allocate any new lonely cats at runtime.</p> <h3 id="commonly-used-instances">Commonly-used instances</h3> <p>You can take the default instance trick further by pre-allocating more than one instance of a class. This admittedly has a narrower range of use cases, but is useful where you have some frequent patterns in the objects you allocate. Let’s consider a temperature sensor which reports temperature measurements in Kelvin and the status of the sensor:</p> <div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">enum</span> <span class="nc">TempSensorStatus</span><span class="k">:</span>
  <span class="kt">case</span> <span class="kt">Normal</span><span class="o">,</span> <span class="nc">TooCold</span><span class="o">,</span> <span class="nc">TooHot</span><span class="o">,</span> <span class="nc">Error</span><span class="o">,</span> <span class="nc">Unresponsive</span>

<span class="c1">// value is in Kelvin</span>
<span class="k">case</span> <span class="k">class</span> <span class="nc">TemperatureReading</span><span class="o">(</span><span class="n">value</span><span class="k">:</span> <span class="kt">Double</span><span class="o">,</span> <span class="n">status</span><span class="k">:</span> <span class="kt">TempSensorStatus</span><span class="o">)</span>

<span class="k">object</span> <span class="nc">TemperatureReading</span><span class="k">:</span>
  <span class="kt">val</span> <span class="kt">tooCold</span> <span class="o">=</span> <span class="nc">TemperatureReading</span><span class="o">(</span><span class="mf">0.0</span><span class="o">,</span> <span class="nv">TempSensorStatus</span><span class="o">.</span><span class="py">TooCold</span><span class="o">)</span>
  <span class="k">val</span> <span class="nv">tooHot</span> <span class="k">=</span> <span class="nc">TemperatureReading</span><span class="o">(</span><span class="mf">1000.0</span><span class="o">,</span> <span class="nv">TempSensorStatus</span><span class="o">.</span><span class="py">TooHot</span><span class="o">)</span>
  <span class="k">val</span> <span class="nv">error</span> <span class="k">=</span> <span class="nc">TemperatureReading</span><span class="o">(</span><span class="nv">Double</span><span class="o">.</span><span class="py">NaN</span><span class="o">,</span> <span class="nv">TempSensorStatus</span><span class="o">.</span><span class="py">Error</span><span class="o">)</span>
  <span class="k">val</span> <span class="nv">unresponsive</span> <span class="k">=</span> <span class="nc">TemperatureReading</span><span class="o">(</span><span class="nv">Double</span><span class="o">.</span><span class="py">NaN</span><span class="o">,</span> <span class="nv">TempSensorStatus</span><span class="o">.</span><span class="py">Unresponsive</span><span class="o">)</span>
</code></pre></div></div> <p>When reading the temperature, you can now just return one of the pre-allocated instances, instead of allocating a new one every time.</p> <p>You could also use an instance cache. When you need to allocate a new object, check if it’s already in your cache. If yes, then return it to the caller. If not, allocate a new one, store it in the cache, and return to the caller. Note that this is much more complex and it may be hard to get any performance improvement out of this. A lot depends on the efficiency of your cache implementation and generally it only makes sense for large classes with many internal allocations and complex initializer logic.</p> <h2 id="reuse-recycle">Reuse, recycle</h2> <p>In the previous section we exploited immutability to our advantage, but the same characteristic can also cause us headaches elsewhere. A good way to avoid allocating objects would be to just reuse what you have already allocated. But, with immutability, you can’t just overwrite the old contents with new data!</p> <h3 id="mutable-collections">Mutable collections</h3> <p>Things are immutable in Scala <a href="https://docs.scala-lang.org/scala3/book/fp-immutable-values.html">for a reason</a>, but there are also <a href="https://docs.scala-lang.org/scala3/book/collections-classes.html">mutable collection types</a> which may make sense in some use cases. So, instead of:</p> <div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">val</span> <span class="nv">twoCats</span><span class="k">:</span> <span class="kt">List</span><span class="o">[</span><span class="kt">String</span><span class="o">]</span> <span class="k">=</span> <span class="nc">List</span><span class="o">(</span><span class="s">"Black cat"</span><span class="o">,</span> <span class="s">"Orange cat (smart)"</span><span class="o">)</span>
<span class="c1">// This allocates new instance of List[String]</span>
<span class="k">val</span> <span class="nv">threeCats</span><span class="k">:</span> <span class="kt">List</span><span class="o">[</span><span class="kt">String</span><span class="o">]</span> <span class="k">=</span> <span class="n">twoCats</span> <span class="o">:+</span> <span class="s">"White cat (dirty)"</span>
</code></pre></div></div> <p>You could use:</p> <div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">val</span> <span class="nv">cats</span> <span class="k">=</span> <span class="nv">collection</span><span class="o">.</span><span class="py">mutable</span><span class="o">.</span><span class="py">ListBuffer</span><span class="o">(</span><span class="s">"Black cat"</span><span class="o">,</span> <span class="s">"Orange cat (smart)"</span><span class="o">)</span>
<span class="n">cats</span> <span class="o">+=</span> <span class="s">"White cat (dirty)"</span>
</code></pre></div></div> <p>…which optimistically saves us one allocation. Of course, if the underlying collection needs to be expanded, it will internally do some allocations.</p> <h3 id="mutable-fields">Mutable fields</h3> <p>You can also consider using mutable fields in your classes, and reusing previously allocated instances that you don’t need anymore. This is useful for example in caches, where instead of allocating a new value for the cache, you can just reuse an object you already had there, as you are going to evict it anyway.</p> <p>Instead of:</p> <div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="k">class</span> <span class="nc">Dog</span><span class="o">(</span><span class="n">name</span><span class="k">:</span> <span class="kt">String</span><span class="o">)</span>

<span class="k">val</span> <span class="nv">dogs</span> <span class="k">=</span> <span class="nv">collection</span><span class="o">.</span><span class="py">mutable</span><span class="o">.</span><span class="py">ArrayBuffer</span><span class="o">(</span><span class="nc">Dog</span><span class="o">(</span><span class="s">"Rex"</span><span class="o">),</span> <span class="nc">Dog</span><span class="o">(</span><span class="s">"Max"</span><span class="o">),</span> <span class="nc">Dog</span><span class="o">(</span><span class="s">"Woof"</span><span class="o">))</span>
<span class="c1">// Replace dog at index 2 with a new one (allocation &amp; deletion)</span>
<span class="nf">dogs</span><span class="o">(</span><span class="mi">2</span><span class="o">)</span> <span class="k">=</span> <span class="nc">Dog</span><span class="o">(</span><span class="s">"Cat-Chaser the Dog"</span><span class="o">)</span>
</code></pre></div></div> <p>You could do:</p> <div class="language-scala highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="k">class</span> <span class="nc">Dog</span><span class="o">(</span><span class="k">var</span> <span class="n">name</span><span class="k">:</span> <span class="kt">String</span><span class="o">)</span>

<span class="k">val</span> <span class="nv">dogs</span> <span class="k">=</span> <span class="nc">Array</span><span class="o">(</span><span class="nc">Dog</span><span class="o">(</span><span class="s">"Rex"</span><span class="o">),</span> <span class="nc">Dog</span><span class="o">(</span><span class="s">"Max"</span><span class="o">),</span> <span class="nc">Dog</span><span class="o">(</span><span class="s">"Woof"</span><span class="o">))</span>
<span class="c1">// Replace dog at index 2 with a new one (no allocation)</span>
<span class="nf">dogs</span><span class="o">(</span><span class="mi">2</span><span class="o">).</span><span class="py">name</span> <span class="k">=</span> <span class="s">"Cat-Chaser the Dog"</span>
</code></pre></div></div> <p>Of course, then you have to be 100% sure that you control the lifetime of these instances and that you won’t, for example, rename someone’s dog by accident. That would be very unethical.</p> <h2 id="summary">Summary</h2> <p>In short: the less you allocate, the better. I hope these tricks will be useful to someone, but please try to use them responsibly and only if there is a clear performance issue.</p> <p>In future parts of this mini-series I will tackle topics like Scala compiler quirks, polymorphism, and inlining. Stay tuned!</p> <h2 id="footnotes">Footnotes</h2> <div class="footnotes" role="doc-endnotes"> <ol> <li id="fn:1"> <p>I highly recommend the blog of Aleksey Shipilëv if you want to learn more about how JVM’s allocation, GC, and other internals work: <a href="https://shipilev.net/jvm/anatomy-quarks/">https://shipilev.net/jvm/anatomy-quarks/</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> </ol> </div>]]></content><author><name></name></author><category term="scala"/><category term="jvm"/><category term="performance"/><category term="optimization"/><category term="garbage-collection"/><category term="gotta-go-fast"/><summary type="html"><![CDATA[In this post, we explore the ways in which you can avoid the overhead of allocating things on the heap in Scala 3.]]></summary></entry><entry><title type="html">Python’s data science ecosystem matures – the case of Polars</title><link href="https://ostrzyciel.eu/blog/2024/pythons-data-science-ecosystem-matures/" rel="alternate" type="text/html" title="Python’s data science ecosystem matures – the case of Polars"/><published>2024-08-14T00:00:00+00:00</published><updated>2024-08-14T00:00:00+00:00</updated><id>https://ostrzyciel.eu/blog/2024/pythons-data-science-ecosystem-matures</id><content type="html" xml:base="https://ostrzyciel.eu/blog/2024/pythons-data-science-ecosystem-matures/"><![CDATA[<p>It simply feels <em>amazing</em> to <strong>finally</strong> get something you were waiting for, after many years. This is a story about Python, data science, and software library design. The star: <a href="https://pola.rs/">Polars</a>.</p> <p><strong>Welcome to the first post on my blog!</strong> 👋 This one is very technical, but I want to cover very diverse topics in the future. Let’s see how it goes.</p> <h2 id="frustrations-with-pandas">Frustrations with Pandas</h2> <p>If you are familiar with data science and Python, then you surely have used the <a href="https://pandas.pydata.org/">Pandas library</a>. It’s a <em>de-facto</em> standard tool for manipulating data frames – tables of data with columns and rows. It has a simple, easy-to-learn API and is well-integrated with the rest of the Python ecosystem. It was also a source of my many frustrations since I started using it, making it one of my most dreaded and, at the same time, most used tools.</p> <p>Pandas is great, but over time, I started noticing more and more of its limitations. One of the most obvious ones is the <strong>inconsistent and confusing API</strong>. I could never properly memorize in which contexts the square braces (<code class="language-plaintext highlighter-rouge">[]</code>) were for selecting columns or rows, or when does a method return a view (and can this view be modified) over a DataFrame or its copy. Filters are janky. The <code class="language-plaintext highlighter-rouge">groupby</code> method has a few different styles of argument specification that I always have to look up – same with <code class="language-plaintext highlighter-rouge">apply</code> and a few others. Indexes (especially multi-level indexes) are confusing to use and don’t seem to be useful in most cases. The list goes on – in general, the API looks like it evolved organically over time, without a specific design philosophy. For me, this makes Pandas harder to use, which slows down work and wastes time.</p> <p>The second big frustration I had was with <strong>handling larger-than-memory datasets</strong>. Pandas always loads everything into memory, making it plain impossible to work on some relatively common workloads, like processing a large dump of sensor data from experiments. This can be alleviated by pre-processing the data using something simpler (like the GNU command-line tools that come with Linux), or rolling out the big guns – something like <a href="https://www.dask.org/">Dask</a>. Instead of using eager evaluation (like Pandas), Dask builds a computation graph (i.e., query plan) that is then lazily evaluated. It can work with data residing in files, has a built-in optimizer, and can even distribute workloads across multiple machines. The issues I had with it was that it had a limited feature set, still inherited the other downsides of Pandas, and added its own layer of complexity. It does work, but for me it’s just <em>wonky</em>.</p> <p>Finally, <strong>Pandas is just slow</strong>. In more complex scenarios, you may need to introduce some Python code to handle a more complicated task, which will instantly reduce the performance dramatically. Even if you avoid this and try the <a href="https://pandas.pydata.org/docs/user_guide/enhancingperf.html">many interesting performance tricks</a>, the processing speed for me always left something to be desired. Dask doesn’t help this much, especially for workloads that are inherently single-threaded.</p> <h2 id="the-broader-context">The broader context</h2> <p>These are not new issues, they are well-known to the community, and many solutions for them were proposed, of varying usefulness.</p> <p>I was often left wondering if I could just use the R language and then all my problems would go away. I was always under the impression that the R data table system was much more consistent and thought-through – same goes for the excellent <a href="https://ggplot2.tidyverse.org/">ggplot2</a> library versus the incredibly messy <a href="https://matplotlib.org/">matplotlib</a> in Python.</p> <p>Although R is a near-perfect ecosystem for data science, it lacks one thing that for me made it impossible to make the switch: the vast collection of libraries written in Python. If I worked only with data science and machine learning, it would be fine – but what about libraries for <a href="https://rdflib.readthedocs.io/en/stable/">knowledge graphs</a>, <a href="https://www.djangoproject.com/">HTTP servers</a>, <a href="https://github.com/earwig/mwparserfromhell">MediaWiki parsers</a>, <a href="https://eclipse.dev/paho/files/paho.mqtt.python/html/client.html">MQTT connectors</a>, <a href="https://opencv.org/">computer vision</a>, <a href="https://www.paramiko.org/">SSH remoting</a>, <a href="https://pypi.org/project/PyVantagePro/">connecting to weather stations over serial</a>, <a href="https://ifcopenshell.org/">processing architectural models (BIM)</a>, and many, many more?<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> I’m sure you can do all of this somehow in R… but I think we can all agree that Python does have an outstanding ecosystem of libraries for basically everything. This alone makes it worth sticking with, at least for me.</p> <h2 id="what-we-needed-all-this-time-polars">What we needed all this time: Polars</h2> <p>Recently I’ve stumbled upon the (relatively) new <strong><a href="https://pola.rs/">Polars library</a></strong>, and it turned out to be everything I needed all this time. It is <a href="https://pola.rs/posts/benchmarks/">incredibly fast</a>, by the virtue of smart vectorization, using columnar storage (same idea as <a href="https://duckdb.org/">DuckDB</a> – another hot topic), and being written in Rust. It has a consistent and well-designed API that actually makes sense. It supports not only lazy evaluation with a query optimizer, but also (experimentally) streaming evaluation. Stream processing is especially interesting, because it could allow for supporting datasets of arbitrary size, even on a single machine. I will come back to the topic of stream processing in a future post.</p> <p>So far I have used Polars in a few data science notebooks, and the results were very good. I had no trouble learning the new syntax and the performance is <a href="https://pola.rs/posts/benchmarks/">just as advertised</a> – blazing-fast. I will certainly replace Pandas with Polars in all my future data science projects.</p> <p>The one thing that Polars does not implement is distributed computing, which is offered by Dask. For me, this is not an issue (more of an advantage, really, due to lower complexity), as I do not work with data that would require distributing the workload. Most of the time I’ve found you can squeeze out a lot of performance by efficiently using the resources of a single node (multithreading, vectorization). With modern CPUs that can crunch vectors with impressive throughput, a lot more can be done on a single node, if you can use this potential. Polars can.</p> <h2 id="the-future">The future</h2> <p>Pandas is a great tool, don’t get me wrong. I very highly respect the incredible amount of work put into it by its developers. But, I see it as a step in the evolution of the data science ecosystem, which <strong>must</strong> move forward to stay relevant. Sometimes, it is better to drop a set of assumptions that don’t work so well anymore, start from scratch, and build something drastically better. I hope that Polars will be this thing.</p> <p>I think the long-term success of Polars will depend on its integration with the wider Python ecosystem. What I really, really, really want to see is an implementation of the <a href="https://seaborn.pydata.org/">Seaborn</a> statistical plotting library over Polars, with a completely redesigned API, akin to R’s ggplot. There is in fact some progress in this direction with Seaborn experimenting with a <a href="https://seaborn.pydata.org/tutorial/objects_interface.html">completely new API for plotting</a>, and there being already some <a href="https://www.rhosignal.com/posts/polars-seaborn/">rudimentary support for plotting from Polars DataFrames</a>. The latter is unfortunately realized by making a full copy of the data to a Pandas DataFrame… so we are not getting any of the performance advantages. I hope the developers of Seaborn will see this opportunity and catch on to it. Then, Python data science will be truly unbeatable. 💪</p> <p>Polars also has an equivalent API in Rust, so it may be possible to introduce support for more languages in the future. I’m especially curious if one compile it to <strong><a href="https://webassembly.org/">WebAssembly</a></strong> and use as a basis for future data-crunching Cloud-Edge-IoT applications <a class="citation" href="#sowinski2023autonomous">(Sowiński et al., 2023)</a>. But this is a rather distant future. Or is it? 🤔</p> <h2 id="footnotes">Footnotes</h2> <div class="footnotes" role="doc-endnotes"> <ol> <li id="fn:1"> <p>Yes, I did use each of these libraries at some point. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> </ol> </div>]]></content><author><name></name></author><category term="python"/><category term="data-science"/><category term="rust"/><category term="polars"/><category term="databases"/><category term="r"/><summary type="html"><![CDATA[It simply feels amazing to finally get something you were waiting for, after many years. This is a story about Python, data science, and software library design. The star: Polars.]]></summary></entry></feed>