<?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://mcopik.github.io/feed.xml" rel="self" type="application/atom+xml"/><link href="https://mcopik.github.io/" rel="alternate" type="text/html" hreflang="en"/><updated>2026-07-13T16:02:00+00:00</updated><id>https://mcopik.github.io/feed.xml</id><title type="html">Marcin Copik</title><subtitle>Personal webpage</subtitle><entry><title type="html">C++ for Serverless - Always Faster?</title><link href="https://mcopik.github.io/blog/2026/lambda-cpp-opencv/" rel="alternate" type="text/html" title="C++ for Serverless - Always Faster?"/><published>2026-03-15T08:00:00+00:00</published><updated>2026-03-15T08:00:00+00:00</updated><id>https://mcopik.github.io/blog/2026/lambda-cpp-opencv</id><content type="html" xml:base="https://mcopik.github.io/blog/2026/lambda-cpp-opencv/"><![CDATA[<p>The motivation to use compiled languages in serverless is straightforward - they typically provide better performance, which translates not only to faster executions but also to lower costs. In <a href="https://github.com/spcl/serverless-benchmarks">SeBS</a>, our benchmarking suite for serverless functions, we have long supported Python and Node.js workloads with automatic build, deployment, and configuration across cloud providers. For a while, we also had initial support for functions written in C++. We have finally ported more benchmarks to C++ - many thanks to Horia for the help and his contributions! - and merged them in pull requests <a href="https://github.com/spcl/serverless-benchmarks/pull/99">#99</a> and <a href="https://github.com/spcl/serverless-benchmarks/pull/293">#293</a>.</p> <p>Our benchmark collection covers a broad spectrum of realistic workloads: web application backends, multimedia processing, utilities, scientific computing, and ML inference. To deploy these in C++, we need to implement several components in SeBS:</p> <ul> <li>Create a general C++ handler that wraps benchmark implementations and connects them to the platform-specific interface.</li> <li>Implement wrappers for storage and other cloud services used by our benchmarks.</li> <li>Provide a set of prebuilt C++ libraries used by the benchmarks.</li> <li>Assemble benchmark code, wrappers, and dependencies into a single deployment, either as a code package or a container.</li> </ul> <p>Before we merged the first PR, I decided to test the performance and compare it against Python. After all, we should expect better performance even if many Python functions rely on libraries that delegate all the heavy lifting to lower-level C and C++ code. The results, as it turns out, were not quite what I expected, and things got even weirder once I started to look into cold startup performance.</p> <h2 id="c-functions-on-aws-lambda">C++ Functions on AWS Lambda</h2> <p>Our function handler is based on the official <a href="https://github.com/awslabs/aws-lambda-cpp">AWS Lambda C++ Runtime</a>. The runtime provides a simple interface: we define a handler that accepts an invocation request, processes it, and returns a response. We wrap this interface so that benchmark functions remain free of platform-specific logic - all of that is contained in our handler. The handler takes care of parsing the JSON payload, timing the benchmark execution, and attaching metadata such as cold start information and a container ID. Let’s walk through the code.</p> <p>We start with global variables that persist across invocations within the same Lambda sandbox and declare the benchmark function signature:</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Global variables that are retained across function invocations</span>
<span class="kt">bool</span> <span class="n">cold_execution</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
<span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">container_id</span> <span class="o">=</span> <span class="s">""</span><span class="p">;</span>
<span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">cold_start_var</span> <span class="o">=</span> <span class="s">""</span><span class="p">;</span>

<span class="c1">// Main benchmark implementation</span>
<span class="n">rapidjson</span><span class="o">::</span><span class="n">Document</span> <span class="nf">function</span><span class="p">(</span><span class="k">const</span> <span class="n">rapidjson</span><span class="o">::</span><span class="n">Value</span><span class="o">&amp;</span> <span class="n">req</span><span class="p">);</span>
</code></pre></div></div> <p>Our very first implementation used AWS-specific types <code class="language-plaintext highlighter-rouge">Aws::Utils::Json::JsonValue</code> and <code class="language-plaintext highlighter-rouge">Aws::Utils::Json::JsonView</code> here, as these were already provided by the AWS SDK that was used by pretty much every benchmark. Later, in PR <a href="https://github.com/spcl/serverless-benchmarks/pull/293">#293</a>, we replaced it by introducing <a href="https://github.com/Tencent/rapidjson"><code class="language-plaintext highlighter-rouge">rapidjson</code></a> as an explicit dependency.</p> <p>The handler begins by parsing the incoming JSON. When the function is invoked through an API Gateway HTTP trigger, the actual payload arrives serialized as a string under the <code class="language-plaintext highlighter-rouge">body</code> key, so we need to parse it a second time. Direct SDK invocations don’t have this wrapping:</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">aws</span><span class="o">::</span><span class="n">lambda_runtime</span><span class="o">::</span><span class="n">invocation_response</span> <span class="nf">handler</span><span class="p">(</span><span class="n">aws</span><span class="o">::</span><span class="n">lambda_runtime</span><span class="o">::</span><span class="n">invocation_request</span> <span class="k">const</span> <span class="o">&amp;</span><span class="n">req</span><span class="p">)</span>
<span class="p">{</span>
  <span class="n">rapidjson</span><span class="o">::</span><span class="n">Document</span> <span class="n">json</span><span class="p">;</span>
  <span class="n">json</span><span class="p">.</span><span class="n">Parse</span><span class="p">(</span><span class="n">req</span><span class="p">.</span><span class="n">payload</span><span class="p">.</span><span class="n">c_str</span><span class="p">());</span>
  <span class="k">if</span><span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="n">HasParseError</span><span class="p">())</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">aws</span><span class="o">::</span><span class="n">lambda_runtime</span><span class="o">::</span><span class="n">invocation_response</span><span class="o">::</span><span class="n">failure</span><span class="p">(</span><span class="s">"Invalid JSON"</span><span class="p">,</span> <span class="s">"application/json"</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="c1">// HTTP trigger with API Gateway sends payload as a serialized JSON</span>
  <span class="c1">// stored under key 'body' in the main JSON</span>
  <span class="c1">// The SDK trigger converts everything for us</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="n">HasMember</span><span class="p">(</span><span class="s">"body"</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">json</span><span class="p">[</span><span class="s">"body"</span><span class="p">].</span><span class="n">IsString</span><span class="p">())</span> <span class="p">{</span>
    <span class="n">rapidjson</span><span class="o">::</span><span class="n">Document</span> <span class="n">body_doc</span><span class="p">;</span>
    <span class="n">body_doc</span><span class="p">.</span><span class="n">Parse</span><span class="p">(</span><span class="n">json</span><span class="p">[</span><span class="s">"body"</span><span class="p">].</span><span class="n">GetString</span><span class="p">());</span>
    <span class="k">if</span><span class="p">(</span><span class="n">body_doc</span><span class="p">.</span><span class="n">HasParseError</span><span class="p">())</span> <span class="p">{</span>
      <span class="k">return</span> <span class="n">aws</span><span class="o">::</span><span class="n">lambda_runtime</span><span class="o">::</span><span class="n">invocation_response</span><span class="o">::</span><span class="n">failure</span><span class="p">(</span><span class="s">"Invalid JSON"</span><span class="p">,</span> <span class="s">"application/json"</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="n">json</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">move</span><span class="p">(</span><span class="n">body_doc</span><span class="p">);</span>
  <span class="p">}</span>
</code></pre></div></div> <p>Then we call the actual benchmark function, measure its execution time, and package the results with the metadata that SeBS needs for analysis:</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">const</span> <span class="k">auto</span> <span class="n">begin</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">chrono</span><span class="o">::</span><span class="n">system_clock</span><span class="o">::</span><span class="n">now</span><span class="p">();</span>
  <span class="k">auto</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">function</span><span class="p">(</span><span class="n">json</span><span class="p">);</span>
  <span class="k">const</span> <span class="k">auto</span> <span class="n">end</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">chrono</span><span class="o">::</span><span class="n">system_clock</span><span class="o">::</span><span class="n">now</span><span class="p">();</span>

  <span class="n">rapidjson</span><span class="o">::</span><span class="n">Document</span> <span class="n">body</span><span class="p">;</span>
  <span class="n">body</span><span class="p">.</span><span class="n">SetObject</span><span class="p">();</span>
  <span class="k">auto</span><span class="o">&amp;</span> <span class="n">alloc</span> <span class="o">=</span> <span class="n">body</span><span class="p">.</span><span class="n">GetAllocator</span><span class="p">();</span>

  <span class="k">auto</span> <span class="n">b</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">chrono</span><span class="o">::</span><span class="n">duration_cast</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">chrono</span><span class="o">::</span><span class="n">microseconds</span><span class="o">&gt;</span><span class="p">(</span><span class="n">begin</span><span class="p">.</span><span class="n">time_since_epoch</span><span class="p">()).</span><span class="n">count</span><span class="p">()</span> <span class="o">/</span> <span class="mf">1000.0</span> <span class="o">/</span> <span class="mf">1000.0</span><span class="p">;</span>
  <span class="k">auto</span> <span class="n">e</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">chrono</span><span class="o">::</span><span class="n">duration_cast</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">chrono</span><span class="o">::</span><span class="n">microseconds</span><span class="o">&gt;</span><span class="p">(</span><span class="n">end</span><span class="p">.</span><span class="n">time_since_epoch</span><span class="p">()).</span><span class="n">count</span><span class="p">()</span> <span class="o">/</span> <span class="mf">1000.0</span> <span class="o">/</span> <span class="mf">1000.0</span><span class="p">;</span>
  <span class="n">body</span><span class="p">.</span><span class="n">AddMember</span><span class="p">(</span><span class="s">"result"</span><span class="p">,</span> <span class="n">ret</span><span class="p">,</span> <span class="n">alloc</span><span class="p">);</span>
  <span class="n">body</span><span class="p">.</span><span class="n">AddMember</span><span class="p">(</span><span class="s">"begin"</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">alloc</span><span class="p">);</span>
  <span class="n">body</span><span class="p">.</span><span class="n">AddMember</span><span class="p">(</span><span class="s">"end"</span><span class="p">,</span> <span class="n">e</span><span class="p">,</span> <span class="n">alloc</span><span class="p">);</span>
  <span class="n">body</span><span class="p">.</span><span class="n">AddMember</span><span class="p">(</span><span class="s">"results_time"</span><span class="p">,</span> <span class="n">e</span> <span class="o">-</span> <span class="n">b</span><span class="p">,</span> <span class="n">alloc</span><span class="p">);</span>
  <span class="n">body</span><span class="p">.</span><span class="n">AddMember</span><span class="p">(</span><span class="s">"request_id"</span><span class="p">,</span> <span class="n">rapidjson</span><span class="o">::</span><span class="n">Value</span><span class="p">(</span><span class="n">req</span><span class="p">.</span><span class="n">request_id</span><span class="p">.</span><span class="n">c_str</span><span class="p">(),</span> <span class="n">alloc</span><span class="p">),</span> <span class="n">alloc</span><span class="p">);</span>
  <span class="n">body</span><span class="p">.</span><span class="n">AddMember</span><span class="p">(</span><span class="s">"is_cold"</span><span class="p">,</span> <span class="n">cold_execution</span><span class="p">,</span> <span class="n">alloc</span><span class="p">);</span>
  <span class="n">body</span><span class="p">.</span><span class="n">AddMember</span><span class="p">(</span><span class="s">"container_id"</span><span class="p">,</span> <span class="n">rapidjson</span><span class="o">::</span><span class="n">Value</span><span class="p">(</span><span class="n">container_id</span><span class="p">.</span><span class="n">c_str</span><span class="p">(),</span> <span class="n">alloc</span><span class="p">),</span> <span class="n">alloc</span><span class="p">);</span>
  <span class="n">body</span><span class="p">.</span><span class="n">AddMember</span><span class="p">(</span><span class="s">"cold_start_var"</span><span class="p">,</span> <span class="n">rapidjson</span><span class="o">::</span><span class="n">Value</span><span class="p">(</span><span class="n">cold_start_var</span><span class="p">.</span><span class="n">c_str</span><span class="p">(),</span> <span class="n">alloc</span><span class="p">),</span> <span class="n">alloc</span><span class="p">);</span>

  <span class="c1">// Switch cold execution after the first one.</span>
  <span class="k">if</span><span class="p">(</span><span class="n">cold_execution</span><span class="p">)</span>
    <span class="n">cold_execution</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>

  <span class="n">rapidjson</span><span class="o">::</span><span class="n">Document</span> <span class="n">final_result</span><span class="p">;</span>
  <span class="n">final_result</span><span class="p">.</span><span class="n">SetObject</span><span class="p">();</span>
  <span class="n">final_result</span><span class="p">.</span><span class="n">AddMember</span><span class="p">(</span><span class="s">"body"</span><span class="p">,</span> <span class="n">body</span><span class="p">,</span> <span class="n">final_result</span><span class="p">.</span><span class="n">GetAllocator</span><span class="p">());</span>

  <span class="n">rapidjson</span><span class="o">::</span><span class="n">StringBuffer</span> <span class="n">buffer</span><span class="p">;</span>
  <span class="n">rapidjson</span><span class="o">::</span><span class="n">Writer</span><span class="o">&lt;</span><span class="n">rapidjson</span><span class="o">::</span><span class="n">StringBuffer</span><span class="o">&gt;</span> <span class="n">writer</span><span class="p">(</span><span class="n">buffer</span><span class="p">);</span>
  <span class="n">final_result</span><span class="p">.</span><span class="n">Accept</span><span class="p">(</span><span class="n">writer</span><span class="p">);</span>

  <span class="k">return</span> <span class="n">aws</span><span class="o">::</span><span class="n">lambda_runtime</span><span class="o">::</span><span class="n">invocation_response</span><span class="o">::</span><span class="n">success</span><span class="p">(</span><span class="n">buffer</span><span class="p">.</span><span class="n">GetString</span><span class="p">(),</span> <span class="s">"application/json"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">main</code> function initializes the AWS SDK, generates a unique container identifier that SeBS uses to distinguish sandboxes, and enters the runtime handler loop. The conditional compilation based on the <code class="language-plaintext highlighter-rouge">SEBS_USE_AWS_SDK</code> flag initializes and shuts down the AWS SDK C++; we use it only when benchmarks explicitly ask for <code class="language-plaintext highlighter-rouge">sdk</code> as a dependency. If a function does not need to access the object storage S3 or NoSQL storage DynamoDB, then there’s no need to link SDK and spend time on initializing it.</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="nf">main</span><span class="p">()</span>
<span class="p">{</span>
<span class="cp">#ifdef SEBS_USE_AWS_SDK
</span>  <span class="n">Aws</span><span class="o">::</span><span class="n">SDKOptions</span> <span class="n">options</span><span class="p">;</span>
  <span class="n">Aws</span><span class="o">::</span><span class="n">InitAPI</span><span class="p">(</span><span class="n">options</span><span class="p">);</span>
<span class="cp">#endif
</span>
  <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span> <span class="n">cold_var</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">getenv</span><span class="p">(</span><span class="s">"cold_start"</span><span class="p">);</span>
  <span class="k">if</span><span class="p">(</span><span class="n">cold_var</span><span class="p">)</span>
    <span class="n">cold_start_var</span> <span class="o">=</span> <span class="n">cold_var</span><span class="p">;</span>
  <span class="n">container_id</span> <span class="o">=</span> <span class="n">boost</span><span class="o">::</span><span class="n">uuids</span><span class="o">::</span><span class="n">to_string</span><span class="p">(</span><span class="n">boost</span><span class="o">::</span><span class="n">uuids</span><span class="o">::</span><span class="n">random_generator</span><span class="p">()());</span>

  <span class="n">aws</span><span class="o">::</span><span class="n">lambda_runtime</span><span class="o">::</span><span class="n">run_handler</span><span class="p">(</span><span class="n">handler</span><span class="p">);</span>

<span class="cp">#ifdef SEBS_USE_AWS_SDK
</span>  <span class="n">Aws</span><span class="o">::</span><span class="n">ShutdownAPI</span><span class="p">(</span><span class="n">options</span><span class="p">);</span>
<span class="cp">#endif
</span>  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div> <p>If a function needs to access cloud services, then we provide platform-agnostic wrappers. In the case of the S3 object storage, we implement access with the official AWS C++ SDK. This generic interface can be reimplemented in the future for a different cloud.</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">std</span><span class="o">::</span><span class="n">tuple</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="p">,</span> <span class="kt">uint64_t</span><span class="o">&gt;</span> <span class="n">sebs</span><span class="o">::</span><span class="n">Storage</span><span class="o">::</span><span class="n">download_file</span><span class="p">(</span>
    <span class="n">Aws</span><span class="o">::</span><span class="n">String</span> <span class="k">const</span> <span class="o">&amp;</span><span class="n">bucket</span><span class="p">,</span> <span class="n">Aws</span><span class="o">::</span><span class="n">String</span> <span class="k">const</span> <span class="o">&amp;</span><span class="n">key</span>
<span class="p">)</span> <span class="p">{</span>
  <span class="n">Aws</span><span class="o">::</span><span class="n">S3</span><span class="o">::</span><span class="n">Model</span><span class="o">::</span><span class="n">GetObjectRequest</span> <span class="n">request</span><span class="p">;</span>
  <span class="n">request</span><span class="p">.</span><span class="n">WithBucket</span><span class="p">(</span><span class="n">bucket</span><span class="p">).</span><span class="n">WithKey</span><span class="p">(</span><span class="n">key</span><span class="p">);</span>
  <span class="k">auto</span> <span class="n">bef</span> <span class="o">=</span> <span class="n">timeSinceEpochMicrosec</span><span class="p">();</span>

  <span class="n">Aws</span><span class="o">::</span><span class="n">S3</span><span class="o">::</span><span class="n">Model</span><span class="o">::</span><span class="n">GetObjectOutcome</span> <span class="n">outcome</span> <span class="o">=</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">_client</span><span class="p">.</span><span class="n">GetObject</span><span class="p">(</span><span class="n">request</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">outcome</span><span class="p">.</span><span class="n">IsSuccess</span><span class="p">())</span> <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">cerr</span> <span class="o">&lt;&lt;</span> <span class="s">"Error: GetObject: "</span> <span class="o">&lt;&lt;</span> <span class="n">outcome</span><span class="p">.</span><span class="n">GetError</span><span class="p">().</span><span class="n">GetMessage</span><span class="p">()</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
    <span class="k">return</span> <span class="p">{</span><span class="s">""</span><span class="p">,</span> <span class="mi">0</span><span class="p">};</span>
  <span class="p">}</span>
  <span class="k">auto</span> <span class="o">&amp;</span><span class="n">s</span> <span class="o">=</span> <span class="n">outcome</span><span class="p">.</span><span class="n">GetResult</span><span class="p">().</span><span class="n">GetBody</span><span class="p">();</span>
  <span class="kt">uint64_t</span> <span class="n">finishedTime</span> <span class="o">=</span> <span class="n">timeSinceEpochMicrosec</span><span class="p">();</span>

  <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="nf">content</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">istreambuf_iterator</span><span class="o">&lt;</span><span class="kt">char</span><span class="o">&gt;</span><span class="p">(</span><span class="n">s</span><span class="p">),</span> <span class="n">std</span><span class="o">::</span><span class="n">istreambuf_iterator</span><span class="o">&lt;</span><span class="kt">char</span><span class="o">&gt;</span><span class="p">());</span>
  <span class="k">return</span> <span class="p">{</span><span class="n">content</span><span class="p">,</span> <span class="n">finishedTime</span> <span class="o">-</span> <span class="n">bef</span><span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div> <p>The S3 client is initialized only once for the entire lifetime of a serverless sandbox, since the first startup comes with significant overhead. In C++, the natural approach would be a static global object - but that doesn’t work here, because <a href="https://github.com/aws/aws-sdk-cpp/issues/2961">the SDK itself relies on static initialization</a>. In C++, the order of static initialization across different translation units is undefined, and mixing our static client with the SDK’s own static objects leads to unpleasant consequences.</p> <p>Instead, we rely on a static local variable, which is initialized the first time the function is called:</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">rapidjson</span><span class="o">::</span><span class="n">Document</span> <span class="nf">function</span><span class="p">(</span><span class="k">const</span> <span class="n">rapidjson</span><span class="o">::</span><span class="n">Value</span><span class="o">&amp;</span> <span class="n">request</span><span class="p">)</span>
<span class="p">{</span>
  <span class="k">static</span> <span class="n">sebs</span><span class="o">::</span><span class="n">Storage</span> <span class="n">client</span> <span class="o">=</span> <span class="n">sebs</span><span class="o">::</span><span class="n">Storage</span><span class="o">::</span><span class="n">get_client</span><span class="p">();</span>
  <span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div> <p>This matters more than it might seem: creating the storage client dynamically would add 7-8 milliseconds to each warm invocation!</p> <h2 id="building-and-deploying-c-functions">Building and Deploying C++ Functions</h2> <p>In Python and Node.js, installing dependencies is simple - pretty much every library can be acquired with a package manager. C++ is a different story: package managers have not been standardized and are nowhere near as popular. We solve this by providing a set of Docker images, each containing a build of a specific dependency such as OpenCV, PyTorch, or igraph. These images also ship the official AWS Lambda C++ Runtime and the AWS C++ SDK.</p> <p>As an example, here is the image that provides Boost, which we use for UUID generation:</p> <div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">ARG</span><span class="s"> BASE_IMAGE</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">${BASE_IMAGE}</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="s">builder</span>
<span class="k">ARG</span><span class="s"> WORKERS</span>
<span class="k">ENV</span><span class="s"> WORKERS=${WORKERS}</span>

<span class="k">RUN </span>dnf <span class="nb">install</span> <span class="nt">-y</span> cmake git gcc-11.5.0-5.amzn2023.0.5.x86_64 gcc-c++-11.5.0-5.amzn2023.0.5.x86_64 make <span class="nb">tar gzip </span>which python-devel
<span class="k">RUN </span>curl <span class="nt">-LO</span> https://archives.boost.io/release/1.79.0/source/boost_1_79_0.tar.gz<span class="se">\
</span>      <span class="o">&amp;&amp;</span> <span class="nb">tar</span> <span class="nt">-xf</span> boost_1_79_0.tar.gz <span class="o">&amp;&amp;</span> <span class="nb">cd </span>boost_1_79_0<span class="se">\
</span>      <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"using gcc : : </span><span class="si">$(</span>which g++<span class="si">)</span><span class="s2"> ;"</span>  <span class="o">&gt;&gt;</span> tools/build/src/user-config.jam<span class="se">\
</span>      <span class="o">&amp;&amp;</span> ./bootstrap.sh <span class="nt">--prefix</span><span class="o">=</span>/opt<span class="se">\
</span>      <span class="o">&amp;&amp;</span> ./b2 <span class="nt">-j</span><span class="k">${</span><span class="nv">WORKERS</span><span class="k">}</span> <span class="nt">--prefix</span><span class="o">=</span>/opt <span class="nv">cxxflags</span><span class="o">=</span><span class="s2">"-fPIC"</span> <span class="nb">link</span><span class="o">=</span>static <span class="nb">install</span>

<span class="k">FROM</span><span class="s"> ${BASE_IMAGE}</span>

<span class="k">COPY</span><span class="s"> --from=builder /opt /opt</span>
</code></pre></div></div> <p>The deployment itself requires no custom logic. Whether we deploy the function as a code package or a container, the steps are almost identical to the Python and Node.js benchmarks. For C++, we create a general-purpose build image that aggregates all dependencies. At deployment time, we compile the benchmark and link it against them. On AWS, this produces a single executable that runs our handler, packaged together with all dynamic dependencies. For container-based deployments, we create a final image containing everything needed for that particular function.</p> <p>This worked quite well. The only hiccup was a version bump of the C++ SDK from <code class="language-plaintext highlighter-rouge">1.11.590</code> to <code class="language-plaintext highlighter-rouge">1.11.642</code>: since the SeBS framework itself is implemented in Python, we upload benchmark inputs through boto3, which uses multi-part upload for large files like the ResNet model for <code class="language-plaintext highlighter-rouge">411.image-recognition</code>. When the C++ function tried to download such an object from S3 storage, it would fail with a <code class="language-plaintext highlighter-rouge">Response checksums mismatch</code>. The root cause was missing support for <a href="https://github.com/aws/aws-sdk-cpp/issues/3496">composite checksums in the AWS C++ SDK</a>, which has since been fixed.</p> <h2 id="performance-of-image-processing-with-opencv">Performance of Image Processing with OpenCV</h2> <p>One of the popular benchmarks in our suite is <code class="language-plaintext highlighter-rouge">210.thumbnailer</code>, which generates thumbnails from images stored in cloud storage. The benchmark provides three primary measurements: time to download a 3.6 MiB image from S3, time to generate a thumbnail, and time to upload the smaller result.</p> <p>First, let’s check how the Python version performs. We download the image from object storage (never saving it to a file), open the binary stream as an image in Pillow, create a thumbnail, and upload the result to object storage:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">PIL</span> <span class="kn">import</span> <span class="n">Image</span>

<span class="c1"># SeBS cloud-agnostic wrappers for object storage
</span><span class="kn">from</span> <span class="n">.</span> <span class="kn">import</span> <span class="n">storage</span>
<span class="n">client</span> <span class="o">=</span> <span class="n">storage</span><span class="p">.</span><span class="n">storage</span><span class="p">.</span><span class="nf">get_instance</span><span class="p">()</span>

<span class="k">def</span> <span class="nf">resize_image</span><span class="p">(</span><span class="n">image_bytes</span><span class="p">,</span> <span class="n">w</span><span class="p">,</span> <span class="n">h</span><span class="p">):</span>

    <span class="k">with</span> <span class="n">Image</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="n">io</span><span class="p">.</span><span class="nc">BytesIO</span><span class="p">(</span><span class="n">image_bytes</span><span class="p">))</span> <span class="k">as</span> <span class="n">image</span><span class="p">:</span>

        <span class="n">image</span><span class="p">.</span><span class="nf">thumbnail</span><span class="p">((</span><span class="n">w</span><span class="p">,</span><span class="n">h</span><span class="p">))</span>
        <span class="n">out</span> <span class="o">=</span> <span class="n">io</span><span class="p">.</span><span class="nc">BytesIO</span><span class="p">()</span>
        <span class="n">image</span><span class="p">.</span><span class="nf">save</span><span class="p">(</span><span class="n">out</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="sh">'</span><span class="s">jpeg</span><span class="sh">'</span><span class="p">)</span>
        <span class="n">out</span><span class="p">.</span><span class="nf">seek</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">out</span>

<span class="c1"># simplified handler
</span><span class="k">def</span> <span class="nf">handler</span><span class="p">(</span><span class="n">bucket</span><span class="p">,</span> <span class="nb">input</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">):</span>

    <span class="n">img</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="nf">download_stream</span><span class="p">(</span><span class="n">bucket</span><span class="p">,</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="nb">input</span><span class="p">,</span> <span class="n">key</span><span class="p">))</span>

    <span class="n">resized</span> <span class="o">=</span> <span class="nf">resize_image</span><span class="p">(</span><span class="n">img</span><span class="p">,</span> <span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">)</span>
    <span class="n">resized_size</span> <span class="o">=</span> <span class="n">resized</span><span class="p">.</span><span class="nf">getbuffer</span><span class="p">().</span><span class="n">nbytes</span>

    <span class="n">key_name</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="nf">upload_stream</span><span class="p">(</span><span class="n">bucket</span><span class="p">,</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">output</span><span class="p">,</span> <span class="n">key</span><span class="p">),</span> <span class="n">resized</span><span class="p">)</span>
</code></pre></div></div> <p>Running this benchmark requires a single command. SeBS automatically handles resource initialization, input upload, code packaging, function creation, and invocation:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sebs.py benchmark invoke 210.thumbnailer test --config config/python.json  --deployment aws

[20:10:13.763893] SeBS-feb5 Created experiment output at /work/serverless/2021/sebs/dev/test-cpp
[20:10:15.069642] AWS.Resources-1475 No resources for AWS found, initialize!
[20:10:15.071344] AWS.Config-379f Using user-provided config for AWS
[20:10:15.108342] AWS.SystemResources-48f3 Initialize S3 storage instance.
[20:10:15.797155] AWS-4df7 Generating unique resource name 2c8f270a
[20:10:16.391190] AWS.S3-3b1c Initialize a new bucket for benchmarks
[20:10:16.839550] AWS.S3-3b1c Created bucket sebs-benchmarks-2c8f270a
</code></pre></div></div> <p>The tool uploads benchmark input data (ten images in this case), and then builds the deployment package using our standardized Docker environment:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[20:10:17.041343] AWS.S3-3b1c Upload sebs/../benchmarks-data/200.multimedia/210.thumbnailer/4_altitude-astrology-astronomy-1819650.jpg to sebs-benchmarks-2c8f270a
# ... and more

[20:10:35.224345] Benchmark-033b Building benchmark 210.thumbnailer. Reason: no cached code package/container.
[20:10:35.240518] Benchmark-033b Docker pull of image spcleth/serverless-benchmarks:build.aws.python.3.10-1.2.0
[20:10:37.436714] Benchmark-033b Docker build of benchmark dependencies in container of image spcleth/serverless-benchmarks:build.aws.python.3.10-1.2.0
[20:10:37.437078] Benchmark-033b Docker mount of benchmark code from path 210.thumbnailer_code/python/3.10/x64/package
[20:10:42.107052] AWS-4df7 Created 210.thumbnailer_code/python/3.10/x64/package/210.thumbnailer.zip archive
[20:10:42.107199] AWS-4df7 Zip archive size 4.604641 MB
[20:10:42.107654] Benchmark-033b Created code package (source hash: 4d735ce143548c7936288c68f04903d5), for run on aws with python:3.10
[20:10:42.108071] Benchmark-6aa6 Caching code package created at 210.thumbnailer_code/python/3.10/x64/package/210.thumbnailer.zip
</code></pre></div></div> <p>Once the code package is ready, we can upload code and create the function. Here, we do it directly with a single API call, but SeBS also supports uploading the code package to object storage when the code package is too large to be used directly:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[20:10:42.110224] AWS-4df7 Creating new function! Reason: function sebs_2c8f270a_210_thumbnailer_python_3_10_x64 not found in cache.
[20:10:43.689136] AWS-4df7 Creating function sebs_2c8f270a_210_thumbnailer_python_3_10_x64 from package cache/210.thumbnailer/aws/python/3.10/x64/package/210.thumbnailer.zip
[20:10:47.046849] AWS-4df7 Waiting for Lambda function to be created...
[20:10:50.773753] AWS-4df7 Lambda function has been created.
[20:10:51.077991] AWS-4df7 Waiting for Lambda function to be updated...
[20:10:52.403601] AWS-4df7 Lambda function has been updated.
[20:10:52.403738] AWS-4df7 Updated configuration of sebs_2c8f270a_210_thumbnailer_python_3_10_x64 function.
[20:10:53.175660] AWS.Resources-1475 Creating HTTP API sebs_2c8f270a_210_thumbnailer_python_3_10_x64-http-api
[20:10:54.362534] AWS-4df7 Created HTTP trigger for sebs_2c8f270a_210_thumbnailer_python_3_10_x64 function. Sleep 5 seconds to avoid cloud errors.
</code></pre></div></div> <p>The benchmark is executed five times, and all executions are saved in the <code class="language-plaintext highlighter-rouge">experiments.json</code> file, including the timing information and details about the function deployment. Additionally, we update the cache with the new function configuration, so that next time we can skip the deployment step and save time.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[20:10:59.364801] SeBS-feb5 Beginning repetition 1/5
[20:11:04.759674] SeBS-feb5 Beginning repetition 2/5
[20:11:05.784416] SeBS-feb5 Beginning repetition 3/5
[20:11:06.809278] SeBS-feb5 Beginning repetition 4/5
[20:11:07.831800] SeBS-feb5 Beginning repetition 5/5
[20:11:08.756281] SeBS-feb5 Save results to experiments.json
[20:11:08.757428] Benchmark-6aa6 Update cached config cache/aws.json
</code></pre></div></div> <p>Afterwards, we <code class="language-plaintext highlighter-rouge">process</code> the results - SeBS queries AWS CloudWatch logs to extract detailed timing information:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sebs.py benchmark process --config config/python.json  --deployment aws

[20:37:18.230999] AWS.Resources-8a82 Using cached resources for AWS
[20:37:18.232417] AWS.Config-62e0 Using cached config for AWS
[20:37:18.271780] AWS-7965 Using existing resource name: 2c8f270a.
[20:37:18.276542] SeBS-60a2 Load results from experiments.json
[20:37:18.767924] AWS.Resources-ced5 Using cached resources for AWS
[20:37:18.769359] AWS.Config-8dc2 Using cached config for AWS
[20:37:19.560642] AWS-7965 Waiting for AWS query to complete ...
[20:37:20.757086] AWS-7965 Received 5 entries, found results for 5 out of 5 invocations
[20:37:20.758410] SeBS-60a2 Save results to results.json
</code></pre></div></div> <p>At 256 MiB of memory (the minimum for this benchmark), the function takes around 540 ms. Since CPU resources on AWS Lambda are allocated proportionally to memory, we can also test with 1769 MiB, which provides a full allocation of 1 vCPU. SeBS makes this trivial - the cached function is updated automatically:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sebs.py benchmark invoke 210.thumbnailer test --config config/python.json  --deployment aws --repetitions 11 --memory 1769

[00:23:09.831662] AWS-93d7 Updating function configuration due to changed attribute memory: cached function has value 256 whereas 1769 has been requested.
[00:23:10.619990] AWS-93d7 Waiting for Lambda function to be updated...
[00:23:11.990141] AWS-93d7 Lambda function has been updated.
[00:23:11.990280] AWS-93d7 Updated configuration of sebs_2c8f270a_210_thumbnailer_python_3_10_x64 function.
</code></pre></div></div> <p>With a full vCPU, the total runtime drops to 150-170 ms. Since SeBS provides fine-grained measurements of individual function stages, we can also inspect the CPU-intensive image resizing step in isolation: it decreases from ~280 ms to 40-45 ms.</p> <p>Now let’s implement the <a href="https://opencv.org/blog/resizing-and-rescaling-images-with-opencv/">C++ counterpart with OpenCV</a>:</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">thumbnailer</span><span class="p">(</span><span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">char</span><span class="o">&gt;&amp;</span> <span class="n">jpeg_data</span><span class="p">,</span> <span class="kt">int64_t</span> <span class="n">width</span><span class="p">,</span> <span class="kt">int64_t</span> <span class="n">height</span><span class="p">,</span> <span class="n">cv</span><span class="o">::</span><span class="n">Mat</span> <span class="o">&amp;</span><span class="n">out</span><span class="p">)</span>
<span class="p">{</span>
  <span class="k">try</span> <span class="p">{</span>
    <span class="n">cv</span><span class="o">::</span><span class="n">Mat</span> <span class="n">in</span> <span class="o">=</span> <span class="n">cv</span><span class="o">::</span><span class="n">imdecode</span><span class="p">(</span><span class="n">jpeg_data</span><span class="p">,</span> <span class="n">cv</span><span class="o">::</span><span class="n">IMREAD_COLOR</span><span class="p">);</span>

    <span class="c1">// Calculate thumbnail size while maintaining aspect ratio</span>
    <span class="kt">int</span> <span class="n">orig_width</span> <span class="o">=</span> <span class="n">in</span><span class="p">.</span><span class="n">cols</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">orig_height</span> <span class="o">=</span> <span class="n">in</span><span class="p">.</span><span class="n">rows</span><span class="p">;</span>

    <span class="c1">// Use smaller scale to fit within bounds</span>
    <span class="kt">double</span> <span class="n">scale_w</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;</span><span class="p">(</span><span class="n">width</span><span class="p">)</span> <span class="o">/</span> <span class="n">orig_width</span><span class="p">;</span>
    <span class="kt">double</span> <span class="n">scale_h</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;</span><span class="p">(</span><span class="n">height</span><span class="p">)</span> <span class="o">/</span> <span class="n">orig_height</span><span class="p">;</span>
    <span class="kt">double</span> <span class="n">scale</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">min</span><span class="p">(</span><span class="n">scale_w</span><span class="p">,</span> <span class="n">scale_h</span><span class="p">);</span>

    <span class="kt">int</span> <span class="n">new_width</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span><span class="p">(</span><span class="n">orig_width</span> <span class="o">*</span> <span class="n">scale</span><span class="p">);</span>
    <span class="kt">int</span> <span class="n">new_height</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span><span class="p">(</span><span class="n">orig_height</span> <span class="o">*</span> <span class="n">scale</span><span class="p">);</span>

    <span class="c1">// Resize image (equivalent to PIL's thumbnail method)</span>
    <span class="n">cv</span><span class="o">::</span><span class="n">resize</span><span class="p">(</span><span class="n">in</span><span class="p">,</span> <span class="n">out</span><span class="p">,</span> <span class="n">cv</span><span class="o">::</span><span class="n">Size</span><span class="p">(</span><span class="n">new_width</span><span class="p">,</span> <span class="n">new_height</span><span class="p">),</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">cv</span><span class="o">::</span><span class="n">INTER_LINEAR</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">catch</span> <span class="p">(</span><span class="k">const</span> <span class="n">cv</span><span class="o">::</span><span class="n">Exception</span> <span class="o">&amp;</span><span class="n">e</span><span class="p">)</span>
  <span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">cerr</span> <span class="o">&lt;&lt;</span> <span class="s">"OpenCV error: "</span> <span class="o">&lt;&lt;</span> <span class="n">e</span><span class="p">.</span><span class="n">what</span><span class="p">()</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="n">rapidjson</span><span class="o">::</span><span class="n">Document</span> <span class="nf">function</span><span class="p">(</span><span class="k">const</span> <span class="n">rapidjson</span><span class="o">::</span><span class="n">Value</span><span class="o">&amp;</span> <span class="n">request</span><span class="p">)</span>
<span class="p">{</span>
  <span class="c1">// Simplified code - download the file.</span>
  <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">input_key</span> <span class="o">=</span> <span class="n">input_key_prefix</span> <span class="o">+</span> <span class="s">"/"</span> <span class="o">+</span> <span class="n">image_name</span><span class="p">;</span>
  <span class="k">auto</span> <span class="n">ans</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">download_file</span><span class="p">(</span><span class="n">bucket_name</span><span class="p">,</span> <span class="n">input_key</span><span class="p">);</span>
  <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">body_str</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">get</span><span class="o">&lt;</span><span class="mi">0</span><span class="o">&gt;</span><span class="p">(</span><span class="n">ans</span><span class="p">);</span>
  <span class="k">auto</span> <span class="n">download_time</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">get</span><span class="o">&lt;</span><span class="mi">1</span><span class="o">&gt;</span><span class="p">(</span><span class="n">ans</span><span class="p">);</span>

  <span class="c1">// Create a thumbnail image and encode it as a JPEG binary blob.</span>
  <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">char</span><span class="o">&gt;</span> <span class="n">vectordata</span><span class="p">(</span><span class="n">body_str</span><span class="p">.</span><span class="n">begin</span><span class="p">(),</span> <span class="n">body_str</span><span class="p">.</span><span class="n">end</span><span class="p">());</span>
  <span class="n">cv</span><span class="o">::</span><span class="n">Mat</span> <span class="n">out_image</span><span class="p">;</span>
  <span class="n">thumbnailer</span><span class="p">(</span><span class="n">vectordata</span><span class="p">,</span> <span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">,</span> <span class="n">out_image</span><span class="p">);</span>
  <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">unsigned</span> <span class="kt">char</span><span class="o">&gt;</span> <span class="n">out_buffer</span><span class="p">;</span>
  <span class="n">cv</span><span class="o">::</span><span class="n">imencode</span><span class="p">(</span><span class="s">".jpg"</span><span class="p">,</span> <span class="n">out_image</span><span class="p">,</span> <span class="n">out_buffer</span><span class="p">);</span>

  <span class="c1">// Determine upload key_name and send data to S3.</span>
  <span class="n">Aws</span><span class="o">::</span><span class="n">String</span> <span class="n">upload_data</span><span class="p">(</span><span class="n">out_buffer</span><span class="p">.</span><span class="n">begin</span><span class="p">(),</span> <span class="n">out_buffer</span><span class="p">.</span><span class="n">end</span><span class="p">());</span>
  <span class="n">client</span><span class="p">.</span><span class="n">upload_random_file</span><span class="p">(</span>
    <span class="n">bucket_name</span><span class="p">,</span> <span class="n">key_name</span><span class="p">,</span> <span class="nb">true</span><span class="p">,</span>
    <span class="k">reinterpret_cast</span><span class="o">&lt;</span><span class="kt">char</span> <span class="o">*&gt;</span><span class="p">(</span><span class="n">out_buffer</span><span class="p">.</span><span class="n">data</span><span class="p">()),</span>
    <span class="n">out_buffer</span><span class="p">.</span><span class="n">size</span><span class="p">()</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div> <p>Pillow is a highly-optimized library that delegates all the heavy lifting to C and C++ code, so it already achieves quite good performance. With a native C++ implementation using OpenCV, we should be at least on par - but that is not what happens. OpenCV takes over 1.6 seconds at 256 MiB and 310 ms at 1769 MiB, significantly worse than the Python version. The resource-constrained Python version at 256 MiB performs <em>better</em> than C++ with a full vCPU!</p> <h3 id="hunting-down-the-regression">Hunting Down the Regression</h3> <p>I went through everything I could think of to explain this:</p> <ul> <li>Verified that all optimization flags are correct (<code class="language-plaintext highlighter-rouge">-O3</code>, <code class="language-plaintext highlighter-rouge">-DNDEBUG</code>).</li> <li>Checked if we need to enable additional optimization, such as AVX2 instructions.</li> <li>Disabled multi-threading in OpenCV and OpenCL support - just in case.</li> <li>Measured the overhead of copying image data between the S3 wrapper and OpenCV.</li> <li>Verified overhead of encoding the image back to JPEG format after resizing; this takes less than 700 microseconds.</li> </ul> <p>I also confirmed that OpenCV was built with the right codecs and hardware support. The CMake configuration summary showed <code class="language-plaintext highlighter-rouge">libjpeg-turbo</code> for JPEG decoding:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#6 44.19 --   Media I/O:
#6 44.19 --     ZLib:                        build (ver 1.2.11)
#6 44.19 --     JPEG:                        libjpeg-turbo (ver 2.0.5-62)
#6 44.19 --     WEBP:                        build (ver encoder: 0x020f)
#6 44.19 --     PNG:                         build (ver 1.6.37)
#6 44.19 --     TIFF:                        build (ver 42 - 4.0.10)
#6 44.20 --     JPEG 2000:                   build (ver 2.3.1)
</code></pre></div></div> <p>CPU dispatching was also set up correctly, with specialized builds for SSE4, AVX2, and AVX-512:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#6 44.19 --   CPU/HW features:
#6 44.19 --     Baseline:                    SSE SSE2 SSE3
#6 44.19 --       requested:                 SSE3
#6 44.19 --     Dispatched code generation:  SSE4_1 SSE4_2 FP16 AVX AVX2 AVX512_SKX
#6 44.19 --       requested:                 SSE4_1 SSE4_2 AVX FP16 AVX2 AVX512_SKX
#6 44.19 --       SSE4_1 (13 files):         + SSSE3 SSE4_1
#6 44.19 --       SSE4_2 (1 files):          + SSSE3 SSE4_1 POPCNT SSE4_2
#6 44.19 --       FP16 (0 files):            + SSSE3 SSE4_1 POPCNT SSE4_2 FP16 AVX
#6 44.19 --       AVX (3 files):             + SSSE3 SSE4_1 POPCNT SSE4_2 AVX
#6 44.19 --       AVX2 (24 files):           + SSSE3 SSE4_1 POPCNT SSE4_2 FP16 FMA3 AVX AVX2
#6 44.19 --       AVX512_SKX (2 files):      + SSSE3 SSE4_1 POPCNT SSE4_2 FP16 FMA3 AVX AVX2 AVX_512F AVX512_COMMON AVX512_SKX
</code></pre></div></div> <p>And the compiler flags looked sane:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#6 44.19 --   C/C++:
#6 44.19 --     Built as dynamic libs?:      YES
#6 44.19 --     C++ standard:                11
#6 44.19 --     C++ Compiler:                /usr/bin/c++  (ver 11.5.0)
#6 44.19 --     C++ flags (Release):         -fsigned-char -W -Wall -Werror=return-type -Werror=non-virtual-dtor -Werror=address -Werror=sequence-point -Wformat -Werror=format-security -Wmissing-declarations -Wundef -Winit-self -Wpointer-arith -Wshadow -Wsign-promo -Wuninitialized -Winit-self -Wsuggest-override -Wno-delete-non-virtual-dtor -Wno-comment -Wimplicit-fallthrough=3 -Wno-strict-overflow -fdiagnostics-show-option -Wno-long-long -pthread -fomit-frame-pointer -ffunction-sections -fdata-sections  -msse -msse2 -msse3 -fvisibility=hidden -fvisibility-inlines-hidden -O3 -DNDEBUG -DNDEBUG
#6 44.19 --     C Compiler:                  /usr/bin/cc
#6 44.19 --     C flags (Release):           -fsigned-char -W -Wall -Werror=return-type -Werror=address -Werror=sequence-point -Wformat -Werror=format-security -Wmissing-declarations -Wmissing-prototypes -Wstrict-prototypes -Wundef -Winit-self -Wpointer-arith -Wshadow -Wuninitialized -Winit-self -Wno-comment -Wimplicit-fallthrough=3 -Wno-strict-overflow -fdiagnostics-show-option -Wno-long-long -pthread -fomit-frame-pointer -ffunction-sections -fdata-sections  -msse -msse2 -msse3 -fvisibility=hidden -O3 -DNDEBUG  -DNDEBUG
</code></pre></div></div> <p>Everything looked correct on paper. After a long debugging session and brainstorming with Gemini, I finally found the culprit: <strong>OpenCV performs full image decoding eagerly</strong>:</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cv</span><span class="o">::</span><span class="n">Mat</span> <span class="n">in</span> <span class="o">=</span> <span class="n">cv</span><span class="o">::</span><span class="n">imdecode</span><span class="p">(</span><span class="n">jpeg_data</span><span class="p">,</span> <span class="n">cv</span><span class="o">::</span><span class="n">IMREAD_COLOR</span><span class="p">);</span>
</code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">Pillow</code>, on the other hand, performs lazy loading - the image is not fully decoded until we access pixel data, which in our case happens only when we call <code class="language-plaintext highlighter-rouge">thumbnail</code>. For a 3.6 MiB JPEG, this makes an enormous difference: the input image is much larger than the thumbnail.</p> <h3 id="bypassing-opencv-with-libjpeg-turbo">Bypassing OpenCV with libjpeg-turbo</h3> <p>I could not find a suitable interface to implement lazy loading in OpenCV. However, OpenCV uses <code class="language-plaintext highlighter-rouge">libjpeg-turbo</code> internally for JPEG decoding, and this library supports scaling down the image <em>during</em> JPEG decompression. It doesn’t support arbitrary sizes, but it supports factors of 1/2, 1/4, and 1/8 - sufficient for our use case. Once we have a much smaller intermediate image, we can use OpenCV for the final resize to the exact thumbnail dimensions:</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">tjhandle</span> <span class="n">tj_handle</span> <span class="o">=</span> <span class="n">tjInitDecompress</span><span class="p">();</span>

<span class="c1">// Read original image properties without decoding the entire image</span>
<span class="kt">int</span> <span class="n">orig_width</span><span class="p">,</span> <span class="n">orig_height</span><span class="p">,</span> <span class="n">subsamp</span><span class="p">,</span> <span class="n">colorspace</span><span class="p">;</span>
<span class="n">tjDecompressHeader3</span><span class="p">(</span>
  <span class="n">tj_handle</span><span class="p">,</span> <span class="k">reinterpret_cast</span><span class="o">&lt;</span><span class="k">const</span> <span class="kt">unsigned</span> <span class="kt">char</span> <span class="o">*&gt;</span><span class="p">(</span><span class="n">jpeg_data</span><span class="p">.</span><span class="n">data</span><span class="p">()),</span>
  <span class="n">jpeg_data</span><span class="p">.</span><span class="n">size</span><span class="p">(),</span> <span class="o">&amp;</span><span class="n">orig_width</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">orig_height</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">subsamp</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">colorspace</span>
<span class="p">);</span>

<span class="c1">// Find the largest possible factor that works for our image</span>
<span class="kt">int</span> <span class="n">scale_num</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span> <span class="n">scale_denom</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">denom</span> <span class="o">:</span> <span class="p">{</span><span class="mi">8</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">1</span><span class="p">})</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">orig_width</span> <span class="o">/</span> <span class="n">denom</span> <span class="o">&gt;=</span> <span class="n">target_width</span> <span class="o">&amp;&amp;</span>
      <span class="n">orig_height</span> <span class="o">/</span> <span class="n">denom</span> <span class="o">&gt;=</span> <span class="n">target_height</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">scale_denom</span> <span class="o">=</span> <span class="n">denom</span><span class="p">;</span>
    <span class="k">break</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// libjpeg-turbo supports these exact fractional scales during decode</span>
<span class="n">tjscalingfactor</span> <span class="n">sf</span> <span class="o">=</span> <span class="p">{</span><span class="n">scale_num</span><span class="p">,</span> <span class="n">scale_denom</span><span class="p">};</span>
<span class="kt">int</span> <span class="n">scaled_width</span> <span class="o">=</span> <span class="n">TJSCALED</span><span class="p">(</span><span class="n">orig_width</span><span class="p">,</span> <span class="n">sf</span><span class="p">);</span>
<span class="kt">int</span> <span class="n">scaled_height</span> <span class="o">=</span> <span class="n">TJSCALED</span><span class="p">(</span><span class="n">orig_height</span><span class="p">,</span> <span class="n">sf</span><span class="p">);</span>

<span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">unsigned</span> <span class="kt">char</span><span class="o">&gt;</span> <span class="n">buffer</span><span class="p">(</span><span class="n">scaled_width</span> <span class="o">*</span> <span class="n">scaled_height</span> <span class="o">*</span> <span class="mi">3</span><span class="p">);</span>
<span class="n">tjDecompress2</span><span class="p">(</span>
  <span class="n">tj_handle</span><span class="p">,</span> <span class="k">reinterpret_cast</span><span class="o">&lt;</span><span class="k">const</span> <span class="kt">unsigned</span> <span class="kt">char</span> <span class="o">*&gt;</span><span class="p">(</span><span class="n">jpeg_data</span><span class="p">.</span><span class="n">data</span><span class="p">()),</span>
  <span class="n">jpeg_data</span><span class="p">.</span><span class="n">size</span><span class="p">(),</span> <span class="n">buffer</span><span class="p">.</span><span class="n">data</span><span class="p">(),</span> <span class="n">scaled_width</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">scaled_height</span><span class="p">,</span>
  <span class="n">TJPF_BGR</span><span class="p">,</span> <span class="n">TJFLAG_FASTDCT</span> <span class="o">|</span> <span class="n">TJFLAG_FASTUPSAMPLE</span>
<span class="p">);</span>

<span class="kt">double</span> <span class="n">scale_w</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;</span><span class="p">(</span><span class="n">target_width</span><span class="p">)</span> <span class="o">/</span> <span class="n">orig_width</span><span class="p">;</span>
<span class="kt">double</span> <span class="n">scale_h</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;</span><span class="p">(</span><span class="n">target_height</span><span class="p">)</span> <span class="o">/</span> <span class="n">orig_height</span><span class="p">;</span>
<span class="kt">double</span> <span class="n">scale</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">min</span><span class="p">(</span><span class="n">scale_w</span><span class="p">,</span> <span class="n">scale_h</span><span class="p">);</span> <span class="c1">// Use smaller scale to fit within bounds</span>

<span class="n">target_width</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span><span class="p">(</span><span class="n">orig_width</span> <span class="o">*</span> <span class="n">scale</span><span class="p">);</span>
<span class="n">target_height</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span><span class="p">(</span><span class="n">orig_height</span> <span class="o">*</span> <span class="n">scale</span><span class="p">);</span>

<span class="n">cv</span><span class="o">::</span><span class="n">Mat</span> <span class="nf">temp</span><span class="p">(</span><span class="n">scaled_height</span><span class="p">,</span> <span class="n">scaled_width</span><span class="p">,</span> <span class="n">CV_8UC3</span><span class="p">,</span> <span class="n">buffer</span><span class="p">.</span><span class="n">data</span><span class="p">());</span>
<span class="c1">// Final scaling step.</span>
<span class="k">if</span> <span class="p">(</span><span class="n">scaled_width</span> <span class="o">!=</span> <span class="n">target_width</span> <span class="o">||</span> <span class="n">scaled_height</span> <span class="o">!=</span> <span class="n">target_height</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">cv</span><span class="o">::</span><span class="n">resize</span><span class="p">(</span><span class="n">temp</span><span class="p">,</span> <span class="n">out</span><span class="p">,</span> <span class="n">cv</span><span class="o">::</span><span class="n">Size</span><span class="p">(</span><span class="n">target_width</span><span class="p">,</span> <span class="n">target_height</span><span class="p">),</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">cv</span><span class="o">::</span><span class="n">INTER_LINEAR</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div> <p>Quick benchmarking on 1769 MiB of memory confirms the improvement: while the OpenCV version with full <code class="language-plaintext highlighter-rouge">imdecode</code> takes over 200 ms just to decompress the image and only a few hundred microseconds to resize it, the new fast decode takes only 40-45 ms.</p> <h3 id="benchmark-results">Benchmark Results</h3> <p>Finally, we use <a href="https://github.com/spcl/serverless-benchmarks/blob/master/docs/experiments.md">SeBS <code class="language-plaintext highlighter-rouge">perf-cost</code> experiment</a> to automatically generate 50 samples for each memory configuration, measuring both cold and warm startups to provide exact measurements. We compute median of timings reported by the cloud provider; SeBS also returns intra-function measurements and timings as observed by the benchmarking client. The coefficient of variation ranged from 4% to 17%, with higher variance observed on larger memory configurations.</p> <p>With the default OpenCV implementation, C++ shows a clear performance regression compared to Python:</p> <table> <thead> <tr> <th>Time (ms)</th> <th>C++ 256 MiB</th> <th>C++ 1769 MiB</th> <th>Python 256 MiB</th> <th>Python 1769 MiB</th> </tr> </thead> <tbody> <tr> <td>Cold, Init</td> <td>351.3</td> <td>347.4</td> <td>113</td> <td>114.5</td> </tr> <tr> <td>Cold, Exec</td> <td>2399</td> <td>414.9</td> <td>4480.3</td> <td>738.1</td> </tr> <tr> <td>Warm, Exec</td> <td>1679.2</td> <td>314.7</td> <td>541.6</td> <td>167.7</td> </tr> </tbody> </table> <p>Even here, one benefit of C++ is already visible: cold executions are consistently faster, since the compiled binary has much less startup work to do than the Python interpreter importing and initializing all libraries. But the execution time tells a different story - C++ is three times slower in the warm case at 256 MiB.</p> <p>However, once we switch to our <code class="language-plaintext highlighter-rouge">libjpeg-turbo</code>-based thumbnailer, the picture changes dramatically:</p> <table> <thead> <tr> <th>Time (ms)</th> <th>C++ 256 MiB</th> <th>C++ 1769 MiB</th> <th>Python 256 MiB</th> <th>Python 1769 MiB</th> </tr> </thead> <tbody> <tr> <td>Cold, Init</td> <td>358.8</td> <td>346.6</td> <td>113</td> <td>114.5</td> </tr> <tr> <td>Cold, Exec</td> <td>848.1</td> <td>198</td> <td>4480.3</td> <td>738.1</td> </tr> <tr> <td>Warm</td> <td>422</td> <td>138.9</td> <td>541.6</td> <td>167.7</td> </tr> </tbody> </table> <p>Not only did we match Python’s performance, but we also slightly improved upon it! Warm execution is now about 17% faster than Python.</p> <p><strong>Note</strong>: the measurements were taken before merging <a href="https://github.com/spcl/serverless-benchmarks/pull/293">pull request #293</a>, which replaced AWS SDK’s JSON library with rapidjson. However, our input and output JSONs are rather small (except for graph benchmarks, which we do not benchmark here), and the performance impact of such change is likely to be very small.</p> <h3 id="cold-startup-performance">Cold Startup Performance</h3> <p>In the results, there is one interesting outlier - cold performance. While the total cold performance (initialization time and execution time) favors C++ significantly, the initialization time is unexpectedly higher. To the best of my knowledge, the <em>init duration</em> measurement includes all time from starting the application process until it connects to the local Lambda endpoint and awaits new invocations. In Python, this includes the pure overhead of starting a Python process, as the entire initialization of AWS SDK (<code class="language-plaintext highlighter-rouge">boto3</code>) happens during the first invocation.</p> <p>In C++, the situation is slightly different. When we link the AWS C++ SDK, we need to initialize before the first use. In our case, we decided to implement it in the <code class="language-plaintext highlighter-rouge">main</code> function for simplicity, and this step could add up to 100 ms to the first invocation. Additionally, we noticed the function needs over 200 ms from the very beginning of initialization - as indicated by the <code class="language-plaintext highlighter-rouge">INIT_START</code> entry in AWS CloudWatch - to the very first line of our <code class="language-plaintext highlighter-rouge">main</code> function. This overhead could be at least partially caused by the static initialization of the AWS C++ SDK, which is difficult to hide unless we try to load the library dynamically with <code class="language-plaintext highlighter-rouge">dlopen</code>.</p> <p>Thus, we also executed the microbenchmark <code class="language-plaintext highlighter-rouge">010.sleep</code> that does not use the SDK. There, the init duration for the cold invocation was about ~150ms, which is lower but still higher than expected: we knew from our <a href="https://mcopik.github.io/projects/cppless">Cppless paper</a> that the initialization overheads should be much lower!</p> <div style="vertical-align:middle; text-align:center"> <a href="/assets/img/blogposts/2026_03_15_cppless.png"> <img class="img-fluid rounded z-depth-1" src="/assets/img/blogposts/2026_03_15_cppless.png" alt="Picture of a Table 17 from the Cppless paper" title="Cold initialization times reported in the Cppless paper."/> </a> </div> <p>These measurements were taken in June 2024. I began by redeploying a simple Cppless example, and the cold startup initialization time was now 90-100 ms:</p> <blockquote> <p>REPORT RequestId: e0b6f5ce-0ff4-4bf6-8068-6695a3680012 Duration: 13.85 ms Billed Duration: 124 ms Memory Size: 1024 MB Max Memory Used: 28 MB Init Duration: 109.77 ms</p> </blockquote> <p>I checked my AWS account, I still had an original deployment of a Cppless benchmark function that has been dormant since July 2024. I executed that function again and the cold initialization time was still around 10-15 ms:</p> <blockquote> <p>REPORT RequestId: c9485409-b690-4299-b64d-faeb045ed46d Duration: 1.20 ms Billed Duration: 13 ms Memory Size: 1024 MB Max Memory Used: 16 MB Init Duration: 11.13 ms</p> </blockquote> <p>But here, the situation gets even stranger. Since it is difficult to isolate all changes that might have affected the build, I decided to first download the code package deployment of the <em>fast</em> function from 2024, and create a new Lambda function with exactly the same code and configuration. I ran it again with the AWS CLI, and lo and behold:</p> <blockquote> <p>REPORT RequestId: 6d971a5b-969b-45ec-b99f-7855e665ba3a Duration: 8.48 ms Billed Duration: 109 ms Memory Size: 1024 MB Max Memory Used: 26 MB Init Duration: 100.31 ms</p> </blockquote> <p>At this point, it looks like whatever is happening during the initialization, was likely caused by the AWS runtime. Did AWS add additional security checks for <em>provided</em> runtime where clients upload unknown binaries? Or did they change the underlying <code class="language-plaintext highlighter-rouge">Amazon Linux 2023</code> runtime, and our old function deployment is still bound to the older release? I’m not sure yet, but it is a very interesting observation that deserves further investigation, and a beautiful example of dealing with limitations of black-box serverless runtimes.</p> <h2 id="summary">Summary</h2> <p>C++ can minimize overheads in serverless, but the exact benefits depend on the workload. As this post shows, naively porting a Python function to C++ does not always guarantee better performance - understanding how each library handles data under the hood matters just as much as the choice of language.</p> <p>Currently, SeBS supports several C++ workloads: a microbenchmark <code class="language-plaintext highlighter-rouge">010.sleep</code>, and four realistic functions - the <code class="language-plaintext highlighter-rouge">210.thumbnailer</code> discussed here, <code class="language-plaintext highlighter-rouge">411.image-recognition</code> (ResNet inference with PyTorch), and the graph operations <code class="language-plaintext highlighter-rouge">501.graph-pagerank</code> and <code class="language-plaintext highlighter-rouge">503.graph-bfs</code>. With C++ support, we can expand towards more scientific and HPC-aligned workloads, conduct experiments that require low overhead of the benchmarking function itself (such as measuring I/O latency and bandwidth in serverless), and explore entirely new types of workloads like GPU-accelerated functions with CUDA.</p> <p>What’s next? We plan to build an ARM cross-compilation toolchain targeting the Graviton CPUs offered on AWS Lambda. On Azure Functions, we want to try the custom runtime support to integrate C++ functions. For Google Cloud, we are looking at Google Cloud Run containers, which are the backbone of the second generation of Cloud Functions. Furthermore, we would like to explore the potential of WebAssembly as a portable compilation target for C++ code, which can be executed in a sandboxed environment on pretty much any platform, including Node.js on Google Cloud Functions and Cloudflare Workers.</p> <p>Interested in more work on serverless C++? Check out <a href="https://mcopik.github.io/projects/cppless/">Cppless</a>, our LLVM-based compiler for serverless functions. In Cppless, we extract C++ lambdas at compile time, build them into standalone binaries, deploy them to AWS Lambda, and invoke them in parallel - all from a single-source program:</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">double</span> <span class="nf">pi_estimate</span><span class="p">(</span><span class="kt">int</span> <span class="n">n</span><span class="p">)</span>
<span class="p">{</span>
  <span class="n">std</span><span class="o">::</span><span class="n">random_device</span> <span class="n">r</span><span class="p">;</span>
  <span class="n">std</span><span class="o">::</span><span class="n">default_random_engine</span> <span class="n">e</span><span class="p">(</span><span class="n">r</span><span class="p">());</span>
  <span class="n">std</span><span class="o">::</span><span class="n">uniform_real_distribution</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;</span> <span class="n">dist</span><span class="p">;</span>

  <span class="kt">int</span> <span class="n">hit</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">n</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">double</span> <span class="n">x</span> <span class="o">=</span> <span class="n">dist</span><span class="p">(</span><span class="n">e</span><span class="p">);</span>
    <span class="kt">double</span> <span class="n">y</span> <span class="o">=</span> <span class="n">dist</span><span class="p">(</span><span class="n">e</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">x</span> <span class="o">*</span> <span class="n">x</span> <span class="o">+</span> <span class="n">y</span> <span class="o">*</span> <span class="n">y</span> <span class="o">&lt;=</span> <span class="mi">1</span><span class="p">)</span>
      <span class="n">hit</span><span class="o">++</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="mi">4</span> <span class="o">*</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;</span><span class="p">(</span><span class="n">hit</span><span class="p">)</span> <span class="o">/</span> <span class="n">n</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span><span class="p">,</span> <span class="kt">char</span><span class="o">*</span><span class="p">[])</span>
<span class="p">{</span>
  <span class="k">const</span> <span class="kt">int</span> <span class="n">n</span> <span class="o">=</span> <span class="mi">100000000</span><span class="p">;</span>
  <span class="k">const</span> <span class="kt">int</span> <span class="n">np</span> <span class="o">=</span> <span class="mi">128</span><span class="p">;</span>

  <span class="n">cppless</span><span class="o">::</span><span class="n">aws_dispatcher</span> <span class="n">dispatcher</span><span class="p">;</span>
  <span class="k">auto</span> <span class="n">aws</span> <span class="o">=</span> <span class="n">dispatcher</span><span class="p">.</span><span class="n">create_instance</span><span class="p">();</span>

  <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">double</span><span class="o">&gt;</span> <span class="n">results</span><span class="p">(</span><span class="n">np</span><span class="p">);</span>
  <span class="k">auto</span> <span class="n">fn</span> <span class="o">=</span> <span class="p">[</span><span class="o">=</span><span class="p">]</span> <span class="p">{</span> <span class="k">return</span> <span class="n">pi_estimate</span><span class="p">(</span><span class="n">n</span> <span class="o">/</span> <span class="n">np</span><span class="p">);</span> <span class="p">};</span>
  <span class="k">for</span> <span class="p">(</span><span class="k">auto</span><span class="o">&amp;</span> <span class="n">result</span> <span class="o">:</span> <span class="n">results</span><span class="p">)</span>
    <span class="n">cppless</span><span class="o">::</span><span class="n">dispatch</span><span class="p">(</span><span class="n">aws</span><span class="p">,</span> <span class="n">fn</span><span class="p">,</span> <span class="n">result</span><span class="p">);</span>
  <span class="n">cppless</span><span class="o">::</span><span class="n">wait</span><span class="p">(</span><span class="n">aws</span><span class="p">,</span> <span class="n">np</span><span class="p">);</span>

  <span class="k">auto</span> <span class="n">pi</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">reduce</span><span class="p">(</span><span class="n">results</span><span class="p">.</span><span class="n">begin</span><span class="p">(),</span> <span class="n">results</span><span class="p">.</span><span class="n">end</span><span class="p">())</span> <span class="o">/</span> <span class="n">np</span><span class="p">;</span>
  <span class="n">std</span><span class="o">::</span><span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="n">pi</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">endl</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div> <p>In the paper, we show that C++ functions provide excellent scalability and performance. We also demonstrate deployment to ARM environments (with a full toolchain integrated into LLVM) and integration into Google Cloud by compiling C++ to WebAssembly, invoked through the officially supported Node.js runtime (a small prototype so far).</p> <p>You can find more details in our <a href="https://mcopik.github.io/projects/cppless/">ACM TACO paper</a>, which was presented in January 2026 at the <a href="https://www.hipeac.net/2026/krakow/">HiPEAC 2026 conference</a>. At the same conference, I also presented the very first <a href="https://www.hipeac.net/2026/krakow/#/program/8250/">tutorial on benchmarking serverless with SeBS</a> - if you missed it, you can find <a href="https://github.com/spcl/serverless-benchmarks">the tutorial materials on GitHuB</a> and try them with <a href="https://github.com/spcl/serverless-benchmarks">SeBS</a>!</p>]]></content><author><name></name></author><category term="serverless"/><category term="c++"/><category term="serverless"/><category term="cloud"/><category term="aws"/><summary type="html"><![CDATA[The motivation to use compiled languages in serverless is straightforward - they typically provide better performance, which translates not only to faster executions but also to lower costs. In SeBS, our benchmarking suite for serverless functions, we have long supported Python and Node.js workloads with automatic build, deployment, and configuration across cloud providers. For a while, we also had initial support for functions written in C++. We have finally ported more benchmarks to C++ - many thanks to Horia for the help and his contributions! - and merged them in pull requests #99 and #293.]]></summary></entry><entry><title type="html">Generating documentation with AI Agents</title><link href="https://mcopik.github.io/blog/2025/llm-documentation/" rel="alternate" type="text/html" title="Generating documentation with AI Agents"/><published>2025-06-17T08:00:00+00:00</published><updated>2025-06-17T08:00:00+00:00</updated><id>https://mcopik.github.io/blog/2025/llm-documentation</id><content type="html" xml:base="https://mcopik.github.io/blog/2025/llm-documentation/"><![CDATA[<p>Software development and maintenance are slightly different in academia than in the industry: there is much more pressure on developing features relevant for new publications and implementing only minimal viable prototypes. For the last five years, I have been maintaining the <a href="#/projects/sebs">the serverless benchmark suite SeBS</a>, which formed my first PhD paper. SeBS evolved into a large codebase over time, supporting many functions, different versions of Python and Node.js, four serverless platforms, different architectures - like x86_64 and arm64 - and deployment modes. We spent significant effort on improving the software quality and making it easier to adopt by other researchers. Over time, we added more features and capabilities, the ongoing support for <a href="#/projects/sebs-flow">serverless workflows in SeBS-Flow</a>. The result of research-driven development and limited resources is predictable: whenever an element of the project is not critical to evaluation in a new paper, its quality suffers.</p> <p>And there’s likely no task more boring and easily discarded than writing documentation. While it does not directly contribute to a new paper, documentation is critical for new users and students who want to work with us and must rely on the existing codebase. Once your project is large and mature, adding documentation to each file can take countless hours. I ended up procrastinating on this task for months since there was always something more exciting to do at the time. But do we still need to spend hours of manual work to add missing docstrings and updating existing ones?</p> <p>In the last few months, the reasoning abilities of LLMs and capabilities of AI agents increased dramatically. LLMs generate entire projects from scratch, create bug fixes, review pull requests - creating documentation should be well within their capabilities. But can they really parse large files, create accurate descriptions, detect outdated docstrings and update them, and provide useful comments? Or will we end up with boilerplate comments that aren’t particularly useful?</p> <p>While I was considering using AI helpers for this task, it still seemed like a mundane process since you had to either keep requesting edits from IDE plugins or manually copy files between your text editor and the LLM’s web interface. However, the appearance of Claude Code in March presented a new vision - a local and semi-automatic AI agent running in a loop that can potentially execute complex tasks without tight human supervision. Could I just give Claude Code it a task of adding missing documentation, let it do its job, and return an hour later to a fully updated documentation? Have we already reached the point where AI agents are capable of conducting a large task by themselves?</p> <p>What I expected</p> <ul> <li>semi-automatic or best fully asynchronous</li> <li>can not only insert generic docstrings - IDes have been able to do it for many years. it should be able to briefly describe function’s logic when it is particularly complex</li> <li>a nice feature would be adding missing type hints</li> </ul> <p>I decided to evaluate several existing AI agents on this task. The analysis is very subjective; the comparison here is not intended to serve as an unbiased and standardized test. Think of it as a record of experience of an average developer. I started with tests on Claude Code, Copilot Pro, and Windsurf on March 10, 2025. Then paper deadlines happened, and the evaluation was postponed indefinitely. The presentation of Google Jules, a remote and closed AI agent, prompted me to conduct an additional test with it on May 21 of the same year. Finally, I re-executed an updated Claude Code and tried Cursor on June 18, 2025. The main prompt I used was: “Please generate missing docstrings and update existing ones since they can be out of date.”</p> <h2 id="claude-code-march-2025">Claude Code (March 2025)</h2> <p>I installed Claude Code on my local machine and started it with the default configuration. I used the preview version <code class="language-plaintext highlighter-rouge">0.2.36</code>. First, Claude will attempt to analyze the entire project structure which can be long an expensive. Fortunately, it will cache the project context in a single file called CLAUDE.md. So far, so good - the description is accurate. It even automatically detected my linting script and tried to apply it to changed files (with varying degree of success).</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>## Build &amp; Run Commands
- Install: `./install.py --aws --azure --gcp --openwhisk --local`
- Run local: `./sebs.py local --config config/example.json`
- Run regression tests: `./sebs.py benchmark regression test --config config/example.json --deployment aws`
- Single test: `./sebs.py benchmark regression test --config config/example.json --deployment aws --benchmark-name &lt;benchmark-name&gt;`
- Unit tests: `tests/test_runner.py --deployment aws`
- Linting: `./tools/linting.py &lt;file_or_directory&gt;`

## Code Style Guidelines
- Max line length: 100 characters
- Formatting: Black with config in `.black.toml`
- Linting: flake8 with config in `.flake8.cfg`
- Type checking: mypy with config in `.mypy.ini`
- Import order: PEP8 style (standard library, third-party, local)
- Naming: snake_case for variables/functions, PascalCase for classes
- Error handling: Use type hints, exception handling with specific exceptions
- Documentation: Docstrings for modules, classes, and functions

## Project Structure
- `sebs/`: Core library code with platform-specific modules
- `benchmarks/`: Benchmark applications
- `tests/`: Test suite
- `docs/`: Project documentation
</code></pre></div></div> <p>I used a simple prompt:</p> <blockquote> <p>For each file in sebs library, please generate missing docstrings and update existing ones since they can be out of date.</p> </blockquote> <h3 id="convenience">Convenience</h3> <p>In Claude Code, you write a direct request to an AI agent, let it do its work in the background, and supervise changes. It is not the ultimate tool for vibe coding - the agent stops to ask if it can make specific edits or run Bash commands, and asks user if this is allowed or if the agent should do something else instead. However, you can usually tell Claude to not ask those questions again, and there is no need to manually select files and lines to be edited, like it is often the case with Copilot-style work in an IDE. Since the tool operates locally in your git repository, there is also no need to copy files between your text editor and the LLM’s web interface.</p> <p>Unfortunately, the entire process didn’t end up being as automatic as I hoped. The agent frequently stopped after processing few files, and needed a gentle push to continue, such as: “Please continue for all remaining files”. According to suggestions, I compacted the context twice which required restarting the entire process.</p> <p>Another problem that I found was the lack of a general vision of how many files are there to process, which ones will be analyzed next, and how many are there left. Since my task was affecting the entire repository, a comprehensive view of the progress would be necessary. In total, Claude Code annotated 20 files and one <code class="language-plaintext highlighter-rouge">__init__.py</code> file.</p> <p><strong>Duration</strong> While I forgot to measure the total time, the timestamps of file modifications indicate that the entire process took roughly one hour and 15 minutes.</p> <h3 id="cost">Cost</h3> <p>At that time, Claude Code was only available through an API key. Claude used two models - 3.7 Sonnet and 3.5 Haiku - but the latter only sporadically (less than 0.05% of input tokens). The total consumption was equal to slightly more than 908,252 input tokens with cache write and 1r5824,851 with cache read. Only 788 tokens were uncached, so I will skip this in estimation. Models produced a total of almost 122,491 output tokens.</p> <p>I loaded my Claude account with \$10, and I ended up with the total cost of almost exactly \$10, with \$8.15 for input tokens and \$1.84 for output tokens. At that point, Claude stopped working since I used all available funds. However, the total charge was later updated to almost \$13.5, and I Claude Code ended up using more funds than were available on my account - this feature was quite surprising. If my memory serves me right, the cost of initial processing of my repository was around $2.5.</p> <h3 id="quality">Quality</h3> <p>Claude created quite decent and comprehensive comments using the Google style for Python docstrings. I never specified the style in my prompt, but the local repository contained an initial configuration of the Sphinx documentation generator, which included the <code class="language-plaintext highlighter-rouge">napoleon</code> extension for parsing Google-style docstrings; perhaps Claude inferred from it that this is the preferred style. The main issue I found was the silent removal of existing and useful comments: the newly generated docstring would replace prior comments and sometimes incorporate bits of information, but often it would remove the most useful parts.</p> <p>Take this for example - this is a very short and informal comment I left in the implementation of a function packing code for AWS Lambda. It is not the best comment - it is too concise and you need a bit of knowledge about the project to understand it. However, it explains why the implementation makes certain decisions - we want to have a standardized deployment procedure for Python functions across many platforms, and here Azure Functions puts certain restrictions on how the code package is structured.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>It would be sufficient to just pack the code and ship it as zip to AWS.
However, to have a compatible function implementation across providers,
we create a small module.
Issue: relative imports in Python when using storage wrapper.
Azure expects a relative import inside a module thus it's easier
to always create a module.

Structure:
function
- function.py
- storage.py
- resources
handler.py
</code></pre></div></div> <p>Claude removed that comment entirely and replaced it with a rather generic docstring that doesn’t explain the reasoning behind the implementation:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Package</span> <span class="n">code</span> <span class="k">for</span> <span class="n">deployment</span> <span class="n">to</span> <span class="n">AWS</span> <span class="n">Lambda</span><span class="p">.</span>

<span class="n">Creates</span> <span class="n">a</span> <span class="n">suitable</span> <span class="n">deployment</span> <span class="n">package</span> <span class="k">with</span> <span class="n">the</span> <span class="n">following</span> <span class="n">structure</span><span class="p">:</span>

<span class="n">function</span><span class="o">/</span>
  <span class="o">-</span> <span class="n">function</span><span class="p">.</span><span class="n">py</span>
  <span class="o">-</span> <span class="n">storage</span><span class="p">.</span><span class="n">py</span>
  <span class="o">-</span> <span class="n">resources</span><span class="o">/</span>
<span class="n">handler</span><span class="p">.</span><span class="n">py</span>

<span class="n">For</span> <span class="n">container</span> <span class="n">deployments</span><span class="p">,</span> <span class="n">builds</span> <span class="n">a</span> <span class="n">Docker</span> <span class="n">image</span> <span class="ow">and</span> <span class="n">pushes</span> <span class="n">it</span> <span class="n">to</span> <span class="n">ECR</span><span class="p">.</span>
<span class="n">For</span> <span class="n">ZIP</span> <span class="n">deployments</span><span class="p">,</span> <span class="n">creates</span> <span class="n">a</span> <span class="n">ZIP</span> <span class="n">package</span> <span class="n">compatible</span> <span class="k">with</span> <span class="n">Lambda</span><span class="p">.</span>

<span class="n">Args</span><span class="p">:</span>
    <span class="n">directory</span><span class="p">:</span> <span class="n">Path</span> <span class="n">to</span> <span class="n">the</span> <span class="n">code</span> <span class="n">directory</span>
    <span class="n">language_name</span><span class="p">:</span> <span class="n">Programming</span> <span class="n">language</span> <span class="nf">name </span><span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="n">g</span><span class="p">.,</span> <span class="sh">'</span><span class="s">python</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">nodejs</span><span class="sh">'</span><span class="p">)</span> 
    <span class="n">language_version</span><span class="p">:</span> <span class="n">Language</span> <span class="nf">version </span><span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="n">g</span><span class="p">.,</span> <span class="sh">'</span><span class="s">3.8</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">14</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">architecture</span><span class="p">:</span> <span class="n">Target</span> <span class="n">CPU</span> <span class="nf">architecture </span><span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="n">g</span><span class="p">.,</span> <span class="sh">'</span><span class="s">x64</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">arm64</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">benchmark</span><span class="p">:</span> <span class="n">Benchmark</span> <span class="n">name</span>
    <span class="n">is_cached</span><span class="p">:</span> <span class="n">Whether</span> <span class="n">code</span> <span class="ow">is</span> <span class="n">already</span> <span class="n">cached</span>
    <span class="n">container_deployment</span><span class="p">:</span> <span class="n">Whether</span> <span class="n">to</span> <span class="n">use</span> <span class="n">container</span> <span class="n">deployment</span>

<span class="n">Returns</span><span class="p">:</span>
    <span class="n">Tuple</span> <span class="n">containing</span><span class="p">:</span>
    <span class="o">-</span> <span class="n">Path</span> <span class="n">to</span> <span class="n">the</span> <span class="n">packaged</span> <span class="nf">code </span><span class="p">(</span><span class="n">ZIP</span> <span class="nb">file</span><span class="p">)</span>
    <span class="o">-</span> <span class="n">Size</span> <span class="n">of</span> <span class="n">the</span> <span class="n">package</span> <span class="ow">in</span> <span class="nb">bytes</span>
    <span class="o">-</span> <span class="n">Container</span> <span class="nc">URI </span><span class="p">(</span><span class="k">if</span> <span class="n">container_deployment</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">otherwise</span> <span class="n">empty</span> <span class="n">string</span><span class="p">)</span>
</code></pre></div></div> <p>At first glance, the new comment is much more verbose and useful. While it correctly explains the structure of the package, it does not explain why we need to create a module with a specific structure. In my opinion, this is the most important part of the comment since this knowledge cannot be trivially recreated just from reading the code.</p> <p>There were also cases where the generated docstring added new knowledge that the LLM gained from reading and understanding the code; the returned container URI in previous comment is a good example. In another function, Claude described the complete behavior of the function:</p> <blockquote> <p>Create or update an AWS Lambda function. If the function already exists, it updates the code and configuration. Otherwise, it creates a new function with the specified parameters.</p> </blockquote> <blockquote> <p>Update an existing AWS Lambda function. Updates the function code and waits for the update to complete. For container deployments, updates the container image. For ZIP deployments, uploads the code package directly or via S3.</p> </blockquote> <p>In another example, Claude fixed an obvious error:</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/blogposts/2025_llm_docs_claude_diff_error-480.webp 480w,/assets/img/blogposts/2025_llm_docs_claude_diff_error-800.webp 800w,/assets/img/blogposts/2025_llm_docs_claude_diff_error-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/blogposts/2025_llm_docs_claude_diff_error.png" class="img-fluid rounded z-depth-1" width="100%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; document.querySelectorAll('.responsive-img-srcset').forEach(function (n) { n.remove(); });"/> </picture> <figcaption class="caption">While this was not part of the initial task, Claude Code was able to detect and fix obvious errors.</figcaption> </figure> <p>Even though my prompt did not mention typing hints, Claude added them to function signature and documentation, which was a nice surprise. I’m not sure if this can be caused can be caused by Claude running my linting script - I found mypy’s reporting of missing type hints to be inconsistent.</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/blogposts/2025_llm_docs_claude_type_hints-480.webp 480w,/assets/img/blogposts/2025_llm_docs_claude_type_hints-800.webp 800w,/assets/img/blogposts/2025_llm_docs_claude_type_hints-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/blogposts/2025_llm_docs_claude_type_hints.png" class="img-fluid rounded z-depth-1 mx-auto d-block" width="50%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; document.querySelectorAll('.responsive-img-srcset').forEach(function (n) { n.remove(); });"/> </picture> <figcaption class="caption">Nice surprise from Claude: adding missing type hints.</figcaption> </figure> <p>And yes, Claude inserted a lot of leading whitespace in the docstrings, which was quite annoying. Finally, sometimes the generated comment was just a waste of expensive tokens:</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/blogposts/2025_llm_docs_claude_diff-480.webp 480w,/assets/img/blogposts/2025_llm_docs_claude_diff-800.webp 800w,/assets/img/blogposts/2025_llm_docs_claude_diff-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/blogposts/2025_llm_docs_claude_diff.png" class="img-fluid rounded z-depth-1 mx-auto d-block" width="50%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; document.querySelectorAll('.responsive-img-srcset').forEach(function (n) { n.remove(); });"/> </picture> <figcaption class="caption">This comment can be useful in far future when humanity loses the ability to divide by 60.</figcaption> </figure> <p>I left the most impressive comment for the last. In our implementation of non-parametric confidence intervals, Claude Code generated a very comprehensive docstring that not only explains the method and its parameters, and even the mathematical background of the algorithm! The comment is not entirely correct, as the book is called <em>Performance Evaluation of Computer and Communication Systems</em> and no paper exists with the title matching Claude’s citation, but it is still a very impressive result.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def ci_le_boudec(alpha: float, times: List[float]) -&gt; Tuple[float, float]:
    """Compute non-parametric confidence interval using Le Boudec's method.

  This function computes a confidence interval for the median of the given
  measurement times using the method described by Le Boudec. This is a
  non-parametric method that does not assume any particular distribution
  of the data.

  Reference:
      J.-Y. Le Boudec, "Methods for the Estimation of the Accuracy of 
      Measurements in Computer Performance Evaluation", 
      Performance Evaluation Review, 2010

  Args:
      alpha: Confidence level (e.g., 0.95 for 95% confidence)
      times: List of measurement times

  Returns:
      A tuple (lower, upper) representing the confidence interval

  Raises:
      AssertionError: If an unsupported confidence level is provided
</code></pre></div></div> <h3 id="summary">Summary</h3> <p>Overall, I found the first preview version of Claude Code to be a promising tool that still lacking in many aspects - it is far from being automatic and it could not complete a large task without close supervision. It lacked notification, so I often returned to Claude only to notice it stopped working some time ago and required another “continue” prompt. Furthermore, it did not provide a good overview of the progress and remaining files to process - it just grabbed a few files for processing every time I asked it to continue the work. Finally, a quick glance at online discussions at that time, including the Reddit’s r/ClaudeAI, showed that I’m not the only one who found Claude Code to be quite pricey.</p> <h2 id="copilot-pro-march-2025">Copilot Pro (March 2025)</h2> <p>As the next tool, I decided to use Copilot Edits in VS Code. This is not a full-fledged AI agent - I had to use the edit mode in the chat and ask to modify specific files. I used the default configuration out of the box, and I tested it on three files that were also edited by Claude Code: main AWS implementation, PerfCost experiment, and a utility module implementing various statistical methods.</p> <p><strong>AWS, GPT-4o</strong></p> <p>Docstrings were in the reStructuredText (reST) style, and limited to a generic one sentence summary with a list of arguments. It lacked a longer overview for more complex functions, and it entirely removed existing comments without including any of their content in the generated docstring. Compared to Claude Code, the results are quite disappointing. Furthermore, it left old function comments which were incorrectly placed - Claude was able to merge with new docstring, evne though it didn’t retain all of the information.</p> <p><strong>PerfCost, GPT-o3-mini</strong></p> <p>This attempt ended with a surprising error message:</p> <blockquote> <p>Failure: “Sorry, the response matched public code so it was blocked. Please rephrase your prompt.”</p> </blockquote> <p>A quick search on <a href="https://stackoverflow.com/questions/79091544/github-copilot-sorry-the-response-matched-public-code-so-it-was-blocked-pleas">StackOverflow</a> shows that it’s a standard error message when Copilot detects that the generated code is similar to existing code in public repositories - which is actually a good feature, and it makes perfect sense why this feature should be opt-out.</p> <p>However, there is one problem here: the code in question is <strong>mine</strong>. I’m editing a file in SeBS repository, and Copilot is blocking my own code. I disabled the feature, but it failed again at the same step.</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/blogposts/2025_llm_docs_copilot_error-480.webp 480w,/assets/img/blogposts/2025_llm_docs_copilot_error-800.webp 800w,/assets/img/blogposts/2025_llm_docs_copilot_error-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/blogposts/2025_llm_docs_copilot_error.png" class="img-fluid rounded z-depth-1" width="100%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; document.querySelectorAll('.responsive-img-srcset').forEach(function (n) { n.remove(); });"/> </picture> <figcaption class="caption">Copilot's block of code plagiarism can be a bit overzealous.</figcaption> </figure> <p><strong>PerfCost, GPT-o1</strong></p> <p>This time, it worked like charm, and it even created a small thinking plan:</p> <blockquote> <p>Step-by-step solution: Add a short docstring to the constructor explaining its purpose and parameters.<br/> Add or update docstrings for methods like prepare, run, compute_statistics, _run_configuration, run_configuration, and process. Use comments to omit all unchanged code.</p> </blockquote> <p>However, the resulting comments from the same problems as previous attempt: they were too short and generic.</p> <p><strong>Statistics, o3-mini</strong></p> <p>Not impressive.</p> <p><strong>Version</strong> Over two months have passed between running this experiment and writing down details of the blogposts, and figuring out the exact version of VSCode extension was surprisingly difficult - the IDE updates all extensions automatically. Fortunately, I was able to find the installation logs of Copilot Chat <code class="language-plaintext highlighter-rouge">0.17.1</code> in <code class="language-plaintext highlighter-rouge">$HOME/.config/Code/logs/*/sharedprocess.log</code>.</p> <h2 id="windsurf-march-2025">Windsurf (March 2025)</h2> <p>I tried the Windsurf IDE, version 1.99.3. It has an AI agent called Cascade, represented as a chat window with a <em>Write</em> mode, where the AI can make changes to your code. The chatbot has a nice history, where you can see all prompts and file actions.</p> <p><strong>AWS, DeepSeek v3</strong></p> <p>Curiously enough, the main prompt didn’t work as expected - AI analyzed the file but made no changes. I had to be much more explicit:</p> <blockquote> <p>Please edit the file: generate missing docstrings and update existing ones since they can be out of date. Please use the Google’s docstring format.`.</p> </blockquote> <p>Then, we can see the full working plan:</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/blogposts/2025_llm_docs_windsurf-480.webp 480w,/assets/img/blogposts/2025_llm_docs_windsurf-800.webp 800w,/assets/img/blogposts/2025_llm_docs_windsurf-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/blogposts/2025_llm_docs_windsurf.png" class="img-fluid rounded z-depth-1" width="100%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; document.querySelectorAll('.responsive-img-srcset').forEach(function (n) { n.remove(); });"/> </picture> <figcaption class="caption">Windsurf's AI agent communicates clearly how prompt translates into actual tasks.</figcaption> </figure> <p>In the end, it ended up updating half of the file and I needed two attempts to finish the whole module. The overall results were similar to Codepilot: very simple comments and no discussion of the actual function behavior. However, it improved on Claude in one aspect: arguments now include the type.</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/blogposts/2025_llm_docs_windsurf_diff-480.webp 480w,/assets/img/blogposts/2025_llm_docs_windsurf_diff-800.webp 800w,/assets/img/blogposts/2025_llm_docs_windsurf_diff-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/blogposts/2025_llm_docs_windsurf_diff.png" class="img-fluid rounded z-depth-1" width="100%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; document.querySelectorAll('.responsive-img-srcset').forEach(function (n) { n.remove(); });"/> </picture> <figcaption class="caption">Compared to Claude Code, Windsurf's AI was able to extend docstrings with Python's type hints.</figcaption> </figure> <p><strong>PerfCost, GPT-o3-mini (medium reasoning)</strong></p> <p>The results are very similar to Copilot.</p> <p><strong>Statistics, 3.7 Sonnet with Thinking</strong></p> <p>Here, the thinking mode seemed to help as Windsurf generated few comments that required understanding code’s behavior:</p> <blockquote> <p>Returns: BasicStats: A named tuple containing: - mean: The arithmetic mean of the times. - median: The median value of the times. - std: The standard deviation of the times. - cv: The coefficient of variation as a percentage (std/mean * 100).</p> </blockquote> <p>instead of Copilot’s more generic:</p> <blockquote> <p>Returns: BasicStats: A named tuple with mean, median, std and cv.</p> </blockquote> <p>Its description of confidence intervals was also more comprehensive:</p> <blockquote> <p>Calculate confidence interval using Le Boudec’s method. This method is a distribution-free confidence interval based on order statistics. It’s more robust to non-normally distributed data than Student’s t-interval.</p> </blockquote> <p><strong>Cost</strong> Windsurf consumed 3.25 User Prompt credits and 4.5 Flow Action credits. I don’t remember how many I had allocated as a new user, and since that time, the pricing model of the agent was significantly simplified - flow action credits are gone.</p> <p><strong>Version</strong> Similar problem to finding the exact version of the VSCode extension - the IDE updates automatically through an APT repository. Fortunately, all logs can be found on Linux in <code class="language-plaintext highlighter-rouge">/var/log/apt/history.log*</code>:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Start-Date: 2025-03-10  18:21:40
Commandline: apt-get upgrade windsurf
Requested-By: mcopik (1000)
Install: windsurf:amd64 (1.4.4-1741285414)
</code></pre></div></div> <h2 id="jules-mayjune-2025">Jules (May/June 2025)</h2> <p>This experimental Google product is a fully automatic and asynchronous AI agent. Compared to Claude, it is remote and fully closed: you integrate with the service through your GitHub repository, select the branch, and provide instructions. Google’s service allocates a virtual machine for the agent to execute, it allows you to review code in the browser, and you finish the work by pushing the code to a branch.</p> <p>I began working with Jules on May, 21. First impressions very positive: the agent produced a full plan of work after two minutes. The plan looked reasonable, but Google had an “auto-approve feature” with a countdown. Even though I was still reviewing the plan, it was automatically approved. I couldn’t find any button to stop the clock.</p> <p>In the end, Jules produced an impressive PR with over 10,000 lines added, and 3,700 lines removed.</p> <p><strong>Convenience</strong> Overall, the entire tool was a bit flaky - in my first use, it failed after roughly one hour and I was not able to restart it due to apparent lack of available virtual machines while I was still able to feed the agent with new tasks. It recovered the next day; the main issue here is that you do not have direct access to the codebase, and the only way to obtain results is to wait for the agent to recover. However, I can’t really complain about fragility of a beta tool that I receive for free. In the end, Jules’ capabilities are very impressive.</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/blogposts/2025_llm_docs_jules_error-480.webp 480w,/assets/img/blogposts/2025_llm_docs_jules_error-800.webp 800w,/assets/img/blogposts/2025_llm_docs_jules_error-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/blogposts/2025_llm_docs_jules_error.png" class="img-fluid rounded z-depth-1" width="100%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; document.querySelectorAll('.responsive-img-srcset').forEach(function (n) { n.remove(); });"/> </picture> <figcaption class="caption">The main downside of a remote AI agent: if it fails, you are left with neither AI help nor the ability to finalize the work by yourself.</figcaption> </figure> <p><strong>Cost</strong> At this moment, Jules is free. As far as I know, it is not possible to find its exact token usage of Gemini models.</p> <h3 id="comparison-against-claude-code">Comparison against Claude Code</h3> <p><strong>AWS</strong> Here, the results are much closer to what Claude Code produced. Function descriptions are longer and explain the internal logic:</p> <blockquote> <p>Claude Create or update an AWS Lambda function. If the function already exists, it updates the code and configuration. Otherwise, it creates a new function with the specified parameters.</p> <p>Jules Create or update an AWS Lambda function. If the function already exists, its configuration and code are updated. Otherwise, a new function is created.</p> </blockquote> <p>However, in other examples, it didn’t do so well:</p> <blockquote> <p>Claude Update an existing AWS Lambda function. Updates the function code and waits for the update to complete. For container deployments, updates the container image. For ZIP deployments, uploads the code package directly or via S3.</p> <p>Jules Update function code and configuration on AWS.</p> </blockquote> <p>Old comments are removed, but Jules didn’t add type hints to function signatures.</p> <p><strong>PerfCost</strong> Claude added a large comment for the entire module, describing accurately experiment and its configuration. Jules did a better job than Copilot and Windsurf, but it still was not as verbose and detailed as Claude. Furthermore, it skipped internal functions like <code class="language-plaintext highlighter-rouge">_run_configuration</code> when generating documentation.</p> <p><strong>Statistics</strong> Something really weird happened here. First, Jules created a correction description of the function, now with a correct citation!</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Calculate a non-parametric confidence interval based on Le Boudec's method.

This method uses order statistics and is suitable for distributions that may
not be normal. It requires a sufficient number of samples (related to z_value calculation).

Reference: "Performance Evaluation of Computer and Communication Systems" by Le Boudec.
</code></pre></div></div> <p>However, this time, the AI decided to significantly rewrite my implementation of the function, which was not part of the task. This short function:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">ci_le_boudec</span><span class="p">(</span><span class="n">alpha</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span> <span class="n">times</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">float</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="n">Tuple</span><span class="p">[</span><span class="nb">float</span><span class="p">,</span> <span class="nb">float</span><span class="p">]:</span>

    <span class="n">sorted_times</span> <span class="o">=</span> <span class="nf">sorted</span><span class="p">(</span><span class="n">times</span><span class="p">)</span>
    <span class="n">n</span> <span class="o">=</span> <span class="nf">len</span><span class="p">(</span><span class="n">times</span><span class="p">)</span>

    <span class="c1"># z(alfa/2)
</span>    <span class="n">z_value</span> <span class="o">=</span> <span class="p">{</span><span class="mf">0.95</span><span class="p">:</span> <span class="mf">1.96</span><span class="p">,</span> <span class="mf">0.99</span><span class="p">:</span> <span class="mf">2.576</span><span class="p">}.</span><span class="nf">get</span><span class="p">(</span><span class="n">alpha</span><span class="p">)</span>
    <span class="k">assert</span> <span class="n">z_value</span>

    <span class="n">low_pos</span> <span class="o">=</span> <span class="n">math</span><span class="p">.</span><span class="nf">floor</span><span class="p">((</span><span class="n">n</span> <span class="o">-</span> <span class="n">z_value</span> <span class="o">*</span> <span class="n">math</span><span class="p">.</span><span class="nf">sqrt</span><span class="p">(</span><span class="n">n</span><span class="p">))</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span>
    <span class="n">high_pos</span> <span class="o">=</span> <span class="n">math</span><span class="p">.</span><span class="nf">ceil</span><span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="p">(</span><span class="n">n</span> <span class="o">+</span> <span class="n">z_value</span> <span class="o">*</span> <span class="n">math</span><span class="p">.</span><span class="nf">sqrt</span><span class="p">(</span><span class="n">n</span><span class="p">))</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span>

    <span class="nf">return </span><span class="p">(</span><span class="n">sorted_times</span><span class="p">[</span><span class="n">low_pos</span><span class="p">],</span> <span class="n">sorted_times</span><span class="p">[</span><span class="n">high_pos</span><span class="p">])</span>
</code></pre></div></div> <p>Changed into this:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">ci_le_boudec</span><span class="p">(</span><span class="n">alpha</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span> <span class="n">times</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">float</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="n">Tuple</span><span class="p">[</span><span class="nb">float</span><span class="p">,</span> <span class="nb">float</span><span class="p">]:</span>
    <span class="sh">"""</span><span class="s">
    Calculate a non-parametric confidence interval based on Le Boudec</span><span class="sh">'</span><span class="s">s method.

    This method uses order statistics and is suitable for distributions that may
    not be normal. It requires a sufficient number of samples (related to z_value calculation).

    Reference: </span><span class="sh">"</span><span class="s">Performance Evaluation of Computer and Communication Systems</span><span class="sh">"</span><span class="s"> by Le Boudec.

    :param alpha: The confidence level (e.g., 0.95 for 95% CI).
    :param times: A list of floating-point time measurements.
    :return: A tuple (lower_bound, upper_bound) of the confidence interval.
             Returns (nan, nan) if the number of samples is too small for the calculation.
    :raises AssertionError: If alpha is not one of the supported values (0.95, 0.99).
    </span><span class="sh">"""</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">times</span><span class="p">:</span>
        <span class="nf">return </span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">nan</span><span class="p">,</span> <span class="n">np</span><span class="p">.</span><span class="n">nan</span><span class="p">)</span>
        
    <span class="n">sorted_times</span> <span class="o">=</span> <span class="nf">sorted</span><span class="p">(</span><span class="n">times</span><span class="p">)</span>
    <span class="n">n</span> <span class="o">=</span> <span class="nf">len</span><span class="p">(</span><span class="n">times</span><span class="p">)</span>

    <span class="c1"># z(alpha/2) - critical value from standard normal distribution
</span>    <span class="c1"># For a two-sided interval with confidence `alpha`, we need z_{1 - (1-alpha)/2} = z_{(1+alpha)/2}
</span>    <span class="c1"># However, the formula used by Le Boudec for indices is n/2 +- z * sqrt(n)/2
</span>    <span class="c1"># The z_value here corresponds to z_{1 - (1-alpha)/2}
</span>    <span class="n">z_critical_value</span> <span class="o">=</span> <span class="p">{</span><span class="mf">0.95</span><span class="p">:</span> <span class="mf">1.96</span><span class="p">,</span> <span class="mf">0.99</span><span class="p">:</span> <span class="mf">2.576</span><span class="p">}.</span><span class="nf">get</span><span class="p">(</span><span class="n">alpha</span><span class="p">)</span>
    <span class="k">assert</span> <span class="n">z_critical_value</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">,</span> <span class="sa">f</span><span class="sh">"</span><span class="s">Unsupported alpha value: </span><span class="si">{</span><span class="n">alpha</span><span class="si">}</span><span class="s">. Supported values are 0.95, 0.99.</span><span class="sh">"</span>

    <span class="c1"># Calculate ranks for lower and upper bounds of the CI for the median
</span>    <span class="c1"># (as per Le Boudec's method for quantiles, here applied to median implicitly)
</span>    <span class="c1"># Note: The formula in the original code seems to be for median CI.
</span>    <span class="c1"># low_pos = floor( (n - z * sqrt(n)) / 2 )
</span>    <span class="c1"># high_pos = ceil( 1 + (n + z * sqrt(n)) / 2 )
</span>    <span class="c1"># These indices are 0-based for the sorted list.
</span>    
    <span class="n">sqrt_n</span> <span class="o">=</span> <span class="n">math</span><span class="p">.</span><span class="nf">sqrt</span><span class="p">(</span><span class="n">n</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">sqrt_n</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span> <span class="c1"># Avoid division by zero if n=0, though caught by earlier check
</span>        <span class="nf">return </span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">nan</span><span class="p">,</span> <span class="n">np</span><span class="p">.</span><span class="n">nan</span><span class="p">)</span>

    <span class="n">val_for_pos</span> <span class="o">=</span> <span class="n">z_critical_value</span> <span class="o">*</span> <span class="n">sqrt_n</span> <span class="o">/</span> <span class="mf">2.0</span>
    
    <span class="c1"># Ensure low_pos and high_pos are within valid array bounds [0, n-1]
</span>    <span class="c1"># The formula can result in indices outside this range if n is too small.
</span>    <span class="n">low_idx</span> <span class="o">=</span> <span class="n">math</span><span class="p">.</span><span class="nf">floor</span><span class="p">(</span><span class="n">n</span> <span class="o">/</span> <span class="mf">2.0</span> <span class="o">-</span> <span class="n">val_for_pos</span><span class="p">)</span>
    <span class="n">high_idx</span> <span class="o">=</span> <span class="n">math</span><span class="p">.</span><span class="nf">ceil</span><span class="p">(</span><span class="n">n</span> <span class="o">/</span> <span class="mf">2.0</span> <span class="o">+</span> <span class="n">val_for_pos</span><span class="p">)</span> <span class="c1"># The original had 1 + n/2 + val_for_pos, usually it's n/2 + z*sqrt(n)/2 for upper rank.
</span>                                              <span class="c1"># Let's stick to a common interpretation of order statistic CIs.
</span>                                              <span class="c1"># The +1 in original might be for 1-based indexing conversion or specific formula variant.
</span>                                              <span class="c1"># For 0-based index, high_idx should be n - 1 - low_idx for symmetric CI around median.
</span>                                              <span class="c1"># Let's use a simpler, more standard approach for quantile CIs if that was the intent,
</span>                                              <span class="c1"># or stick to the provided formula if it's a specific known method.
</span>                                              <span class="c1"># Re-evaluating the original formula:
</span>                                              <span class="c1"># low_pos_orig = math.floor((n - z_critical_value * math.sqrt(n)) / 2)
</span>                                              <span class="c1"># high_pos_orig = math.ceil(1 + (n + z_critical_value * math.sqrt(n)) / 2)
</span>                                              <span class="c1"># These indices are 0-based. high_pos_orig includes an extra +1.
</span>                                              <span class="c1"># Let's assume the formula is as intended.
</span>                                              <span class="c1"># Need to ensure low_pos &gt;=0 and high_pos &lt; n
</span>
    <span class="n">low_pos_calculated</span> <span class="o">=</span> <span class="n">math</span><span class="p">.</span><span class="nf">floor</span><span class="p">((</span><span class="n">n</span> <span class="o">-</span> <span class="n">z_critical_value</span> <span class="o">*</span> <span class="n">sqrt_n</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span>
    <span class="c1"># The `1 +` in high_pos seems to make it 1-based then implicitly 0-based by list access.
</span>    <span class="c1"># Or it's part of a specific formula variant.
</span>    <span class="c1"># If it's rank k, then index is k-1.
</span>    <span class="c1"># Let's ensure indices are valid.
</span>    <span class="n">high_pos_calculated</span> <span class="o">=</span> <span class="n">math</span><span class="p">.</span><span class="nf">ceil</span><span class="p">(</span><span class="mi">1</span> <span class="o">+</span> <span class="p">(</span><span class="n">n</span> <span class="o">+</span> <span class="n">z_critical_value</span> <span class="o">*</span> <span class="n">sqrt_n</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span>
    
    <span class="c1"># Clamp indices to valid range [0, n-1]
</span>    <span class="n">final_low_idx</span> <span class="o">=</span> <span class="nf">max</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">low_pos_calculated</span><span class="p">)</span>
    <span class="n">final_high_idx</span> <span class="o">=</span> <span class="nf">min</span><span class="p">(</span><span class="n">n</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="n">high_pos_calculated</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="c1"># -1 if high_pos_calculated was 1-based rank
</span>
    <span class="k">if</span> <span class="n">final_low_idx</span> <span class="o">&gt;</span> <span class="n">final_high_idx</span> <span class="ow">or</span> <span class="n">final_high_idx</span> <span class="o">&gt;=</span> <span class="n">n</span> <span class="ow">or</span> <span class="n">final_low_idx</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">:</span> <span class="c1"># Check validity
</span>        <span class="c1"># This happens if n is too small for the given alpha
</span>        <span class="nf">return </span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">nan</span><span class="p">,</span> <span class="n">np</span><span class="p">.</span><span class="n">nan</span><span class="p">)</span>

    <span class="nf">return </span><span class="p">(</span><span class="n">sorted_times</span><span class="p">[</span><span class="n">final_low_idx</span><span class="p">],</span> <span class="n">sorted_times</span><span class="p">[</span><span class="n">final_high_idx</span><span class="p">])</span>
</code></pre></div></div> <p>While one could argue that now we have a slightly better error checking, the entire implementation looks like a novel. We have 15 lines of code dedicated to derivation of <code class="language-plaintext highlighter-rouge">low_idx</code> and <code class="language-plaintext highlighter-rouge">high_idx</code>, which are not even used used. Furthermore, the comments look like an artifact of AI’s reasoning process.</p> <h3 id="reviewing-jules-pr">Reviewing Jules PR</h3> <p>Reviewing a 10,000 line PR is not neither fast nor easy. Since the AI generated the code, perhaps it could also review it?</p> <p><strong>Copilot Pro</strong> First, I tried Copilot Pro. It was able to review the PR and provide a few comments, but it was not able to detect any issues with the code. Surprisingly, it produced only two comments but both suppressed by low confidence. However, both referred to an outstanding FIXME included in the comment.</p> <p><strong>CodeRabbit AI</strong></p> <p>We have been using CodeRabbit for quite some time. While the tool can be a bit intrusive, it does produce some interesting comments. It found the dead code introduced by Jules in the unnecessary overhaul of statistical computations.</p> <p>It was also able to find new bugs, where Jules fixed incorrect return type but did not insert necessary imports. This happened multiple times through the codebase:</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/blogposts/2025_llm_docs_coderabbit_review-480.webp 480w,/assets/img/blogposts/2025_llm_docs_coderabbit_review-800.webp 800w,/assets/img/blogposts/2025_llm_docs_coderabbit_review-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/blogposts/2025_llm_docs_coderabbit_review.png" class="img-fluid rounded z-depth-1" width="100%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; document.querySelectorAll('.responsive-img-srcset').forEach(function (n) { n.remove(); });"/> </picture> <figcaption class="caption"></figcaption> </figure> <p>This bug is difficult to explain - just a random typo. AI are more human than one might think.</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/blogposts/2025_llm_docs_coderabbit_review_typo-480.webp 480w,/assets/img/blogposts/2025_llm_docs_coderabbit_review_typo-800.webp 800w,/assets/img/blogposts/2025_llm_docs_coderabbit_review_typo-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/blogposts/2025_llm_docs_coderabbit_review_typo.png" class="img-fluid rounded z-depth-1" width="100%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; document.querySelectorAll('.responsive-img-srcset').forEach(function (n) { n.remove(); });"/> </picture> <figcaption class="caption"></figcaption> </figure> <p>Another interesting bug. Most classes in the project inherit from a base class that defines custom loggers with redirect to files or stdout, as well as coloring options. Jules learned the <code class="language-plaintext highlighter-rouge">self.logging</code> pattern, but it failed to notice that this class does not inherit from it.</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/blogposts/2025_llm_docs_coderabbit_review_logging-480.webp 480w,/assets/img/blogposts/2025_llm_docs_coderabbit_review_logging-800.webp 800w,/assets/img/blogposts/2025_llm_docs_coderabbit_review_logging-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/blogposts/2025_llm_docs_coderabbit_review_logging.png" class="img-fluid rounded z-depth-1" width="100%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; document.querySelectorAll('.responsive-img-srcset').forEach(function (n) { n.remove(); });"/> </picture> <figcaption class="caption"></figcaption> </figure> <p>Why Jules did that ?</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/blogposts/2025_llm_docs_coderabbit_review_cache-480.webp 480w,/assets/img/blogposts/2025_llm_docs_coderabbit_review_cache-800.webp 800w,/assets/img/blogposts/2025_llm_docs_coderabbit_review_cache-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/blogposts/2025_llm_docs_coderabbit_review_cache.png" class="img-fluid rounded z-depth-1" width="100%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; document.querySelectorAll('.responsive-img-srcset').forEach(function (n) { n.remove(); });"/> </picture> <figcaption class="caption"></figcaption> </figure> <p>Before, this was the constructor signature:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">config</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">cache</span><span class="p">:</span> <span class="n">Cache</span><span class="p">):</span>
</code></pre></div></div> <p>AI agent decided that this pattern is <em>unusual</em> and rewrote code to use a plain dictionary instead. Perhaps this is not the best software engineering pattern, but I never asked Jules to fix such problems.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># cache is passed to __init__ but not stored as self.cache directly, used for cached_config in deserialize
# It's unusual for a config object to hold the cache client itself.
</span>
<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">config_values</span><span class="p">:</span> <span class="nb">dict</span><span class="p">,</span> <span class="n">cached_config_for_resources</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">dict</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">):</span>
</code></pre></div></div> <h3 id="updating-the-pr-june">Updating the PR (June)</h3> <p>Finally, I had to finish the PR. As far as I know, there is currently no method of asking Jules to fix PR comments. I have only seen this feature so far in Copilot Pro+, which is rather pricey - $390 per annum.</p> <p>Thus, I gave Jules two additional tasks in June to complete the work. First, I asked it to add missing type information. This ended prematurely due to apparent task duration limits:</p> <blockquote> <p>Processing of files directly under the <code class="language-plaintext highlighter-rouge">sebs</code> root directory was partially completed. I reviewed <code class="language-plaintext highlighter-rouge">sebs/statistics.py</code> and found it to be already well-typed and documented. I did not process other files in the <code class="language-plaintext highlighter-rouge">sebs</code> root (e.g., <code class="language-plaintext highlighter-rouge">sebs.py</code>, <code class="language-plaintext highlighter-rouge">benchmark.py</code>, <code class="language-plaintext highlighter-rouge">cache.py</code>, <code class="language-plaintext highlighter-rouge">config.py</code>, <code class="language-plaintext highlighter-rouge">types.py</code>, <code class="language-plaintext highlighter-rouge">utils.py</code>) in this commit due to reaching the end of the current work cycle.</p> </blockquote> <p>Once Jules completed this work after an explicit request, I asked the agent to update the docstrings to Google’s style.</p> <p>Recover from problems: I had to explicitly agree to it as a user, but notifications help here since the agent is not waiting for hours to continue the work.</p> <blockquote> <p>I’ve encountered a small issue. While converting docstrings to Google style in sebs/aws/aws.py, I had trouble with the docstring for the disable_rich_output method. It’s a very simple docstring and already largely compliant, so I seem to be struggling with a minor (or no-op) change. To avoid getting stuck, I’ve decided to skip converting this specific docstring. The rest of the sebs/aws directory has been successfully processed for Google-style docstrings. I’m now ready to proceed with converting the docstrings in the sebs/azure directory as originally planned. Please let me know if you have any concerns about skipping this one problematic docstring, or if I should proceed.</p> </blockquote> <h2 id="claude-code-june-2025">Claude Code (June 2025)</h2> <p>On June, 4, Anthropic opened Claude Code to users with their Pro plan - albeit with usage limits. On June 18, I updated Claude Code to version <code class="language-plaintext highlighter-rouge">1.0.27</code> and changed its configuration to use my subscriptions instead of the Anthropic API. I let it continue the unfinished work from March, but gave it more tasks.</p> <p>Now we have a really nice plan and overview of the entire work to be done! This addresses one of the main issues I had with the first version of Claude Code.</p> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/blogposts/2025_llm_docs_claude_todo-480.webp 480w,/assets/img/blogposts/2025_llm_docs_claude_todo-800.webp 800w,/assets/img/blogposts/2025_llm_docs_claude_todo-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/blogposts/2025_llm_docs_claude_todo.png" class="img-fluid rounded z-depth-1" width="100%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; document.querySelectorAll('.responsive-img-srcset').forEach(function (n) { n.remove(); });"/> </picture> <figcaption class="caption">The current version of Claude Code provides a comprehensive and clear status of execution.</figcaption> </figure> <figure> <picture> <source class="responsive-img-srcset" srcset="/assets/img/blogposts/2025_llm_docs_claude_error_recovery-480.webp 480w,/assets/img/blogposts/2025_llm_docs_claude_error_recovery-800.webp 800w,/assets/img/blogposts/2025_llm_docs_claude_error_recovery-1400.webp 1400w," type="image/webp" sizes="95vw"/> <img src="/assets/img/blogposts/2025_llm_docs_claude_error_recovery.png" class="img-fluid rounded z-depth-1" width="100%" height="auto" data-zoomable="" loading="lazy" onerror="this.onerror=null; document.querySelectorAll('.responsive-img-srcset').forEach(function (n) { n.remove(); });"/> </picture> <figcaption class="caption">The current version of Claude Code provides a comprehensive and clear status of execution.</figcaption> </figure> <p>Claude managed to modify 30 files until it reached usage limits, which reset every 5 hours. After resetting the limit, it was able to complete the work.</p> <h3 id="reviewing-the-pr">Reviewing the PR</h3> <p>I first applied our linting pipeline that consists of <code class="language-plaintext highlighter-rouge">blake</code>, <code class="language-plaintext highlighter-rouge">flake8</code>, and <code class="language-plaintext highlighter-rouge">mypy</code>. Mypy was able to find many fixes.</p> <p>Here, the argument type should be more general than just a <code class="language-plaintext highlighter-rouge">Dict</code>. I replaced the second argument with <code class="language-plaintext highlighter-rouge">typing.Mapping</code>.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">def</span> <span class="nf">update</span><span class="p">(</span><span class="n">d</span><span class="p">:</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">],</span> <span class="n">u</span><span class="p">:</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]:</span>
    <span class="sh">"""</span><span class="s">Recursively update nested dictionary with another dictionary.

    This function performs deep merge of two dictionaries, updating nested
    dictionary values rather than replacing them entirely.

    Args:
        d (Dict[str, Any]): The target dictionary to update.
        u (Dict[str, Any]): The source dictionary with updates.

    Returns:
        Dict[str, Any]: The updated dictionary.
    </span><span class="sh">"""</span>
    <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">u</span><span class="p">.</span><span class="nf">items</span><span class="p">():</span>
        <span class="k">if</span> <span class="nf">isinstance</span><span class="p">(</span><span class="n">v</span><span class="p">,</span> <span class="n">collections</span><span class="p">.</span><span class="n">abc</span><span class="p">.</span><span class="n">Mapping</span><span class="p">):</span>
            <span class="n">d</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">=</span> <span class="nf">update</span><span class="p">(</span><span class="n">d</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="p">{}),</span> <span class="n">v</span><span class="p">)</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">d</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">=</span> <span class="n">v</span>
    <span class="k">return</span> <span class="n">d</span>
</code></pre></div></div> <p>This one was weird - it should be <code class="language-plaintext highlighter-rouge">typing.Any</code></p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nd">@abstractmethod</span>
    <span class="k">def</span> <span class="nf">serialize</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">any</span><span class="p">]:</span>
</code></pre></div></div> <p>Other bugs were coming from an old and well known issue in mypy - name shadowing.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="n">ret</span> <span class="o">=</span> <span class="n">cli_instance</span><span class="p">.</span><span class="nf">execute</span><span class="p">(</span>
      <span class="sh">"</span><span class="s">az storage account show-connection-string --name {}</span><span class="sh">"</span><span class="p">.</span><span class="nf">format</span><span class="p">(</span><span class="n">account_name</span><span class="p">)</span>
  <span class="p">)</span>
  <span class="n">ret</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="nf">loads</span><span class="p">(</span><span class="n">ret</span><span class="p">.</span><span class="nf">decode</span><span class="p">(</span><span class="sh">"</span><span class="s">utf-8</span><span class="sh">"</span><span class="p">))</span>
  <span class="n">connection_string</span> <span class="o">=</span> <span class="n">ret</span><span class="p">[</span><span class="sh">"</span><span class="s">connectionString</span><span class="sh">"</span><span class="p">]</span>
</code></pre></div></div> <p>This returns bytes and mypy complains later <code class="language-plaintext highlighter-rouge">No overload variant of "__getitem__" of "bytes" matches argument type "str"</code></p> <p>Let claude code run in a loop - that worked very well; just tell to keep running the linting scriopt and fix issues until it’s all green. I didn’t try it with tests yet but it look very promising. It was quite good at parsing and understnading the output of <code class="language-plaintext highlighter-rouge">mypy</code> and <code class="language-plaintext highlighter-rouge">flake8</code>, and it was able to fix most of the issues.</p> <p>So, how does Jules compares to Claude Code? Overall, Claude Code was more comprehensive. In few comments, Jules ended up providing more useful information.</p> <p>Sometimes, Jules was actually more efficient:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">def</span> <span class="nf">create_table</span><span class="p">(</span>
        <span class="n">self</span><span class="p">,</span> <span class="n">benchmark</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">name</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">primary_key</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">secondary_key</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="sh">"""</span><span class="s">
        Create a DynamoDB table for a benchmark.
        Generates a unique table name using resource ID, benchmark name, and provided name.
        Handles cases where the table already exists or is being created.
        Uses PAY_PER_REQUEST billing mode.
        In contrast to the hierarchy of database objects in Azure (account -&gt; database -&gt; container)
        and GCP (database per benchmark), we need to create unique table names here.

        </span><span class="sh">"""</span>
</code></pre></div></div> <p>and Claude Code</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">def</span> <span class="nf">create_table</span><span class="p">(</span>
        <span class="n">self</span><span class="p">,</span> <span class="n">benchmark</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">name</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">primary_key</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">secondary_key</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="sh">"""</span><span class="s">
        Create a DynamoDB table for benchmark data.
        Creates a DynamoDB table with a unique name for the benchmark. Unlike
        Azure (account -&gt; database -&gt; container) and GCP (database per benchmark),
        AWS requires unique table names across the account.

        </span><span class="sh">"""</span>
</code></pre></div></div> <p>Sometimes Jules would make stuff up:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">def</span> <span class="nf">code_bucket</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">benchmark</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">storage_client</span><span class="p">:</span> <span class="n">S3</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="sh">"""</span><span class="s">
        Get or assign the S3 bucket for code deployment.
        If a bucket is not already assigned to this function, it retrieves
        the deployment bucket from the S3 storage client.
        :param benchmark: Name of the benchmark (used by storage_client if creating a new bucket, though typically not needed here).
        :param storage_client: S3 client instance.
        :return: The name of the S3 bucket used for code deployment.
        </span><span class="sh">"""</span>
        <span class="n">self</span><span class="p">.</span><span class="n">bucket</span> <span class="o">=</span> <span class="n">storage_client</span><span class="p">.</span><span class="nf">get_bucket</span><span class="p">(</span><span class="n">Resources</span><span class="p">.</span><span class="n">StorageBucketType</span><span class="p">.</span><span class="n">DEPLOYMENT</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">bucket</span>
</code></pre></div></div> <p>Sometimes, both systems correctly recognize that the existing docstring was very outdated and removed the incorrect description.]</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
  <span class="n">Create</span> <span class="n">a</span> <span class="n">client</span> <span class="n">instance</span> <span class="k">for</span> <span class="n">cloud</span> <span class="n">storage</span><span class="p">.</span> <span class="n">When</span> <span class="n">benchmark</span> <span class="ow">and</span> <span class="n">buckets</span>
  <span class="n">parameters</span> <span class="n">are</span> <span class="n">passed</span><span class="p">,</span> <span class="n">then</span> <span class="n">storage</span> <span class="ow">is</span> <span class="n">initialized</span> <span class="k">with</span> <span class="n">required</span> <span class="n">number</span>
  <span class="n">of</span> <span class="n">buckets</span><span class="p">.</span> <span class="n">Buckets</span> <span class="n">may</span> <span class="n">be</span> <span class="n">created</span> <span class="ow">or</span> <span class="n">retrieved</span> <span class="k">from</span> <span class="n">cache</span><span class="p">.</span>
</code></pre></div></div> <p>This code logic has been removed some time ago, and the parameters are no longer in function signature. Claude Code just removed it; Jules just changed to the following <code class="language-plaintext highlighter-rouge">When benchmark and buckets parameters are passed (implicitly via config), storage is initialized with the required number of buckets. </code></p> <p>Which is incorrect.</p> <p>Exceptions are also difficult; for this code</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        <span class="k">try</span><span class="p">:</span>
            <span class="c1"># this is incredible
</span>            <span class="c1"># https://github.com/boto/boto3/issues/125
</span>            <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">region</span> <span class="o">!=</span> <span class="sh">"</span><span class="s">us-east-1</span><span class="sh">"</span><span class="p">:</span>
                <span class="n">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="nf">create_bucket</span><span class="p">(</span>
                    <span class="n">Bucket</span><span class="o">=</span><span class="n">bucket_name</span><span class="p">,</span>
                    <span class="n">CreateBucketConfiguration</span><span class="o">=</span><span class="p">{</span><span class="sh">"</span><span class="s">LocationConstraint</span><span class="sh">"</span><span class="p">:</span> <span class="n">self</span><span class="p">.</span><span class="n">region</span><span class="p">},</span>
                <span class="p">)</span>
            <span class="k">else</span><span class="p">:</span>
                <span class="c1"># This is incredible x2 - boto3 will not throw exception if you recreate
</span>                <span class="c1"># a bucket in us-east-1
</span>                <span class="c1"># https://github.com/boto/boto3/issues/4023
</span>                <span class="n">buckets</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">list_buckets</span><span class="p">()</span>
                <span class="k">if</span> <span class="n">bucket_name</span> <span class="ow">in</span> <span class="n">buckets</span><span class="p">:</span>
                    <span class="n">self</span><span class="p">.</span><span class="n">logging</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span>
                        <span class="sa">f</span><span class="sh">"</span><span class="s">The bucket </span><span class="si">{</span><span class="n">bucket_name</span><span class="si">}</span><span class="s"> not successful; it exists already</span><span class="sh">"</span>
                    <span class="p">)</span>
                    <span class="k">raise</span> <span class="nc">RuntimeError</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Bucket </span><span class="si">{</span><span class="n">bucket_name</span><span class="si">}</span><span class="s"> already exists</span><span class="sh">"</span><span class="p">)</span>
                <span class="n">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="nf">create_bucket</span><span class="p">(</span><span class="n">Bucket</span><span class="o">=</span><span class="n">bucket_name</span><span class="p">)</span>

            <span class="n">self</span><span class="p">.</span><span class="n">logging</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="sh">"</span><span class="s">Created bucket {}</span><span class="sh">"</span><span class="p">.</span><span class="nf">format</span><span class="p">(</span><span class="n">bucket_name</span><span class="p">))</span>
        <span class="k">except</span> <span class="n">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">exceptions</span><span class="p">.</span><span class="n">BucketAlreadyExists</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
            <span class="n">self</span><span class="p">.</span><span class="n">logging</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">The bucket </span><span class="si">{</span><span class="n">bucket_name</span><span class="si">}</span><span class="s"> exists already in region </span><span class="si">{</span><span class="n">self</span><span class="p">.</span><span class="n">region</span><span class="si">}</span><span class="s">!</span><span class="sh">"</span><span class="p">)</span>
            <span class="k">raise</span> <span class="n">e</span>
        <span class="k">except</span> <span class="n">self</span><span class="p">.</span><span class="n">client</span><span class="p">.</span><span class="n">exceptions</span><span class="p">.</span><span class="n">ClientError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
            <span class="n">self</span><span class="p">.</span><span class="n">logging</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span>
                <span class="sa">f</span><span class="sh">"</span><span class="s">The bucket </span><span class="si">{</span><span class="n">bucket_name</span><span class="si">}</span><span class="s"> not successful; perhaps it exists already in a region </span><span class="sh">"</span>
                <span class="sa">f</span><span class="sh">"</span><span class="s"> different from </span><span class="si">{</span><span class="n">self</span><span class="p">.</span><span class="n">region</span><span class="si">}</span><span class="s">?</span><span class="sh">"</span>
            <span class="p">)</span>
            <span class="n">self</span><span class="p">.</span><span class="n">logging</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="n">e</span><span class="p">)</span>
            <span class="k">raise</span> <span class="n">e</span>

</code></pre></div></div> <p>We raise three types of exceptions - two boto3 native ones, and one craeted RuntimeError. The latter is creatred by us because of a <a href="https://github.com/boto/boto3/issues/4023">bug in boto3</a> - it fails to throw an exception when trying to create an already existing bucket in <code class="language-plaintext highlighter-rouge">us-east-1</code> region.</p> <p>Jules generated docstring with <code class="language-plaintext highlighter-rouge">RuntimeError</code> only and <code class="language-plaintext highlighter-rouge">ClientError</code>, but it did not include <code class="language-plaintext highlighter-rouge">BucketAlreadyExists</code> exception.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">:</span><span class="n">raises</span> <span class="nb">RuntimeError</span><span class="p">:</span> <span class="n">If</span> <span class="n">bucket</span> <span class="n">creation</span> <span class="nf">fails </span><span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="n">g</span><span class="p">.,</span> <span class="n">already</span> <span class="n">exists</span> <span class="n">globally</span><span class="p">).</span>
</code></pre></div></div> <p>whereas claude got the correct exceptions and the reasons when they are thrown</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Raises</span><span class="p">:</span>
    <span class="n">BucketAlreadyExists</span><span class="p">:</span> <span class="n">If</span> <span class="n">bucket</span> <span class="n">already</span> <span class="n">exists</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">same</span> <span class="n">region</span>
    <span class="n">ClientError</span><span class="p">:</span> <span class="n">If</span> <span class="n">bucket</span> <span class="n">creation</span> <span class="n">fails</span> <span class="k">for</span> <span class="n">other</span> <span class="n">reasons</span>
    <span class="nb">RuntimeError</span><span class="p">:</span> <span class="n">If</span> <span class="n">bucket</span> <span class="n">already</span> <span class="n">exists</span> <span class="ow">in</span> <span class="n">us</span><span class="o">-</span><span class="n">east</span><span class="o">-</span><span class="mi">1</span> <span class="n">region</span>
</code></pre></div></div> <p>For azure blob storage, Jules decided to chjange semantics of fucntion - for no good reason. Not even new docstring discusses this semantics.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Previously we didn't use the overwrite keyword, which defaults to False
</span><span class="n">client</span><span class="p">.</span><span class="nf">upload_blob</span><span class="p">(</span><span class="n">upload_file</span><span class="p">,</span> <span class="n">overwrite</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="c1"># type: ignore
</span></code></pre></div></div> <p>Curiously enough, Claude still missed processing few files and required additional prompts to finish the work. Some files were finished partially.</p> <p>Another example of changing semantics</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>

    <span class="nd">@property</span>
    <span class="k">def</span> <span class="nf">container_uri</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="k">assert</span> <span class="n">self</span><span class="p">.</span><span class="n">_container_uri</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">_container_uri</span>

        <span class="nd">@property</span>
    <span class="k">def</span> <span class="nf">container_uri</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span> <span class="c1"># Changed from str to Optional[str]
</span>        <span class="sh">"""</span><span class="s">The URI of the container image, if applicable for containerized deployment.</span><span class="sh">"""</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">_container_uri</span>
</code></pre></div></div> <p>Here, Jules was better</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>

    <span class="nd">@property</span>
    <span class="k">def</span> <span class="nf">language_name</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="sh">"""</span><span class="s">The string name of the programming language (e.g., </span><span class="sh">"</span><span class="s">python</span><span class="sh">"</span><span class="s">).</span><span class="sh">"""</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">_language</span><span class="p">.</span><span class="n">value</span>

    <span class="nd">@property</span>
    <span class="k">def</span> <span class="nf">language_version</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span> <span class="c1"># Added return type
</span>        <span class="sh">"""</span><span class="s">The version of the programming language runtime (e.g., </span><span class="sh">"</span><span class="s">3.8</span><span class="sh">"</span><span class="s">).</span><span class="sh">"""</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">_language_version</span>


    
    <span class="nd">@property</span>
    <span class="k">def</span> <span class="nf">language_name</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="sh">"""</span><span class="s">
        Get the name of the programming language.
        Returns:
            str: Name of the language
        </span><span class="sh">"""</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">_language</span><span class="p">.</span><span class="n">value</span>

    <span class="nd">@property</span>
    <span class="k">def</span> <span class="nf">language_version</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="sh">"""</span><span class="s">
        Get the version of the programming language.
        Returns:
            str: Version of the language
        </span><span class="sh">"""</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">_language_version</span>
</code></pre></div></div> <p>Sometimes JUles would just end up rewriting entire functions to replace names with more descriptive ones. for example, changes to <code class="language-plaintext highlighter-rouge">sebs/benchmark.py</code> include 617 lines from Claude Code but 1,201 lines modified by Jules</p> <p>Another example - Jules was smart, Claude Code was not.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
    <span class="nd">@staticmethod</span>
    <span class="k">def</span> <span class="nf">typename</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="sh">"""</span><span class="s">Return the type name of this class (used for logging context).</span><span class="sh">"""</span>
        <span class="c1"># This seems to be a placeholder or misnamed, as Cache is not a Benchmark.
</span>        <span class="c1"># It should probably be "Cache" or similar if used for logging context.
</span>        <span class="k">return</span> <span class="sh">"</span><span class="s">Cache</span><span class="sh">"</span> <span class="c1"># Changed from "Benchmark" for clarity
</span>
        <span class="nd">@staticmethod</span>
    <span class="k">def</span> <span class="nf">typename</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="sh">"""</span><span class="s">Get the typename for this cache.
        Returns:
            str: The cache type name.
        </span><span class="sh">"""</span>
        <span class="k">return</span> <span class="sh">"</span><span class="s">Benchmark</span><span class="sh">"</span>
</code></pre></div></div> <p>wtf jules?</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        <span class="n">points</span> <span class="o">=</span> <span class="nf">linspace</span><span class="p">(</span>
            <span class="n">settings</span><span class="p">[</span><span class="sh">"</span><span class="s">payload_begin</span><span class="sh">"</span><span class="p">],</span>
            <span class="n">settings_</span><span class="p">[</span><span class="sh">"</span><span class="s">payload_end</span><span class="sh">"</span><span class="p">],</span>
            <span class="n">settings</span><span class="p">[</span><span class="sh">"</span><span class="s">payload_points</span><span class="sh">"</span><span class="p">],</span>
        <span class="p">)</span>
</code></pre></div></div> <p>Sometimes Claude would get it completely wrong. We use that counter to update vlaues of enviornment variables inisde function container, forcing a restart of active containers.</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nd">@property</span>
    <span class="k">def</span> <span class="nf">cold_start_counter</span><span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
        <span class="sh">"""</span><span class="s">
        Get the cold start counter.
        This counter is used in function name generation to help force cold starts
        by creating new function instances with different names.
        Returns:
            int: The current cold start counter value
        </span><span class="sh">"""</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">_cold_start_counter</span>
</code></pre></div></div> <h2 id="summary-1">Summary</h2> <p>I think I will have to do everything myself in the end</p>]]></content><author><name></name></author><category term="serverless"/><category term="sebs"/><category term="serverless"/><category term="llm"/><category term="cloud"/><summary type="html"><![CDATA[Software development and maintenance are slightly different in academia than in the industry: there is much more pressure on developing features relevant for new publications and implementing only minimal viable prototypes. For the last five years, I have been maintaining the the serverless benchmark suite SeBS, which formed my first PhD paper. SeBS evolved into a large codebase over time, supporting many functions, different versions of Python and Node.js, four serverless platforms, different architectures - like x86_64 and arm64 - and deployment modes. We spent significant effort on improving the software quality and making it easier to adopt by other researchers. Over time, we added more features and capabilities, the ongoing support for serverless workflows in SeBS-Flow. The result of research-driven development and limited resources is predictable: whenever an element of the project is not critical to evaluation in a new paper, its quality suffers.]]></summary></entry><entry><title type="html">Cross-compiling C++ to serverless ARM</title><link href="https://mcopik.github.io/blog/2025/lambda-arm-cpp/" rel="alternate" type="text/html" title="Cross-compiling C++ to serverless ARM"/><published>2025-03-11T08:00:00+00:00</published><updated>2025-03-11T08:00:00+00:00</updated><id>https://mcopik.github.io/blog/2025/lambda-arm-cpp</id><content type="html" xml:base="https://mcopik.github.io/blog/2025/lambda-arm-cpp/"><![CDATA[<p>AWS Lambda is arguably the most popular serverless service. Lambda functions support natively several programming languages, such as Python, Node.js, Java or Golang. However, other languages can be supported through <code class="language-plaintext highlighter-rouge">custom</code> runtime, where we deploy a function with a bootstrap script to execute.</p> <p>Lambda has a unique execution environment for C++. The runtime is based on Amazon Linux 2, which is a CentOS derivative. In languages like Python or Node.js, shipping the function code to serverless is easy Since there is no standardized packaging environment in C++, we</p> <p>There are various methods of creating cross-compilation environments, such as <a href="https://github.com/crosstool-ng/crosstool-ng">crostool-ng</a>. Alternatively, the entire compilation can be executed within a Docker container that contains the entire toolchain for a different platform.</p> <p>no standardized build - so no way to tell if dependency A does not brin ganother one.</p> <p>First, since we are on x64 Linux and we want to compile Lambda</p> <p>First, we need to get the entire toolchain from. To simplify this process, we will use an existing, containerized cross-compilation toolchain and locate it under the <code class="language-plaintext highlighter-rouge">arm-sysroot</code> path on our local disk: We could obtain form the <a href="dockross"><code class="language-plaintext highlighter-rouge">dockcross</code></a> containers.</p> <p>However, since we need to ship all system libraries as suggested by AWS, and we know the operating environment of Lambda on AWS, we can instead extract the corss-compilation toolchain from an AWS container. This will simplify the deployment process since we will be able to skip <code class="language-plaintext highlighter-rouge">libc</code> and its dependencies, as we know that this library will be available at the destination.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>arm-sysroot <span class="o">&amp;&amp;</span> docker run <span class="nt">--rm</span> dockcross/linux-arm64  <span class="nb">tar</span> <span class="nt">--dereference</span> <span class="nt">-czf</span> - /usr/xcc | <span class="nb">tar</span> <span class="nt">-xzf</span> - <span class="nt">-C</span> arm-sysroot
</code></pre></div></div>]]></content><author><name></name></author><category term="c++"/><category term="c++"/><category term="serverless"/><category term="llvm"/><category term="aws"/><summary type="html"><![CDATA[AWS Lambda is arguably the most popular serverless service. Lambda functions support natively several programming languages, such as Python, Node.js, Java or Golang. However, other languages can be supported through custom runtime, where we deploy a function with a bootstrap script to execute.]]></summary></entry><entry><title type="html">Google Summer of Code 2023</title><link href="https://mcopik.github.io/blog/2023/gsoc/" rel="alternate" type="text/html" title="Google Summer of Code 2023"/><published>2023-03-31T08:00:00+00:00</published><updated>2023-03-31T08:00:00+00:00</updated><id>https://mcopik.github.io/blog/2023/gsoc</id><content type="html" xml:base="https://mcopik.github.io/blog/2023/gsoc/"><![CDATA[<p>Google Summer of Code is a unique opportunity to engage with the open-source community. To work on exciting projects with established organizations, candidates must submit a proposal that outlines their project idea and explains how they plan to accomplish project goals during the coding period. I participated in the GSoC program as a student twice, I mentored students, and now I am proud to co-organize the GSoC program at <a href="https://summerofcode.withgoogle.com/programs/2023/organizations/scalable-parallel-computing-laboratory">Scalable Parallel Computing Laboratory (SPCL) @ ETH Zurich</a>. My GSoC experience spanned multiple fields, from Java-based model checking, through high-performance computing in C++, up to serverless computing in Python and C++. However, regardless of the language and technology, there is one rule that applies to all projects: <strong>the proposal is the most important factor when evaluating a prospective student.</strong> It is the only way you have to convince mentors that you are the right person for the job.</p> <p>The proposal should be structured around a few critical questions. First, it needs to explain clearly <strong>what problem you are trying to solve?</strong>. Then, you need to show that <strong>the problem is important</strong> for the organization and to you - are you excited to work on this project? Finally, the proposals should <strong>convince mentors that you will succeed</strong>. Did you check all dependencies? Did you check related work in other projects? Did you think about potential problems? All these factors demonstrate that you are a well-prepared and promising candidate.</p> <ul> <li> <p><strong>Check existing resources.</strong> GSoC has been going for over a decade now, and there are countless articles and blogposts that help you to prepare for the program. Definitely check <a href="https://google.github.io/gsocguides/student/writing-a-proposal">the official GSoC guide</a> on writing proposals.</p> </li> <li> <p><strong>Learn from the others.</strong> Hundreds of students have conducted successful GSoC projects in the past, and many of their proposals are available online. These can be invaluable sources of good practices that will help you to write a more convincing proposal. For example, the official GSoC guideline includes <a href="https://google.github.io/gsocguides/student/proposal-example-1">proposal examples</a>. Other organizations shared their proposals as well - you can find many examples in the <a href="https://blogs.python-gsoc.org/en/">Python Software Foundation</a> and in <a href="https://github.com/prondubuisi/accepted-gsoc-proposals">this GitHub collection</a>.</p> </li> <li> <p><strong>Be precise and concise.</strong> Proposals cannot be too short since they need to contain necessary information on project goals and timeline. However, there is no need to explain every library you are planning to use in detail. Instead, focus on aspects that are critical to your project.</p> </li> <li> <p><strong>Do your research.</strong> Every project has many potential pitfalls - what if the selected library is incompatible with the rest of the project? Is the framework you plan to use the best option, or are other faster, leaner, and more modern alternatives? Explaining your design choices in the proposal shows that you have thought through the idea and won’t be surprised by a foreseeable problem.</p> </li> <li> <p><strong>Procrastination does not help.</strong>. It is uncommon for someone writes a fantastic proposal in the last few hours before the deadline. Prepare the first draft early and share it with mentors to receive feedback. The best papers and proposals require multiple iterations of corrections and feedback to become really good.</p> </li> <li> <p><strong>Sell your skills.</strong> Emphasize skills most relevant to the project, and focus on experiences demonstrating your successes in managing projects of similar scope and complexity.</p> </li> <li> <p><strong>Last, but not least - be original.</strong> The recent introduction of ChatGPT made it possible to “write” long form documents with minimal effort. While many AI-powered writing can help you to write better English and polish your sentences, you should not let the AI do all of the work. And obviously plagiarism is not acceptable. Do not send proposals where you simply copied contents from another project or from a paper.</p> </li> </ul>]]></content><author><name></name></author><category term="open-source"/><category term="serverles"/><category term="cloud"/><category term="open-source"/><summary type="html"><![CDATA[Google Summer of Code is a unique opportunity to engage with the open-source community. To work on exciting projects with established organizations, candidates must submit a proposal that outlines their project idea and explains how they plan to accomplish project goals during the coding period. I participated in the GSoC program as a student twice, I mentored students, and now I am proud to co-organize the GSoC program at Scalable Parallel Computing Laboratory (SPCL) @ ETH Zurich. My GSoC experience spanned multiple fields, from Java-based model checking, through high-performance computing in C++, up to serverless computing in Python and C++. However, regardless of the language and technology, there is one rule that applies to all projects: the proposal is the most important factor when evaluating a prospective student. It is the only way you have to convince mentors that you are the right person for the job.]]></summary></entry><entry><title type="html">Installing FetchContent targets in CMake</title><link href="https://mcopik.github.io/blog/2023/cmake/" rel="alternate" type="text/html" title="Installing FetchContent targets in CMake"/><published>2023-03-31T08:00:00+00:00</published><updated>2023-03-31T08:00:00+00:00</updated><id>https://mcopik.github.io/blog/2023/cmake</id><content type="html" xml:base="https://mcopik.github.io/blog/2023/cmake/"><![CDATA[<p>The <a href="https://cmake.org/cmake/help/latest/module/FetchContent.html"><code class="language-plaintext highlighter-rouge">FetchContent</code></a> module is the easiest and most efficient way of adding dependencies to your CMake project. In contrast to <code class="language-plaintext highlighter-rouge">ExternalProject</code>, it fetches the dependency at the configuration time, which makes it easier to discover imported targets and verify that you are linking your executables and libraries correctly. Thus, you can now easily add to your C/C++ project dependencies that you do not expect to be available on the user’s system - libraries, SDKs, and internal tools such as testing frameworks. In fact, the first example shown in CMake documentation uses the <code class="language-plaintext highlighter-rouge">gtest</code> framework:</p> <div class="language-cmake highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">FetchContent_Declare</span><span class="p">(</span>
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        703bd9caab50b139428cea1aaff9974ebee5742e <span class="c1"># release-1.10.0</span>
<span class="p">)</span>
<span class="nf">FetchContent_MakeAvailable</span><span class="p">(</span>googletest<span class="p">)</span>
</code></pre></div></div> <p>And it works great! We don’t have to do anything else, as CMake will fetch the <code class="language-plaintext highlighter-rouge">gtest</code> release, configure it, and build at runtime. We only have to link our test targets against imported <code class="language-plaintext highlighter-rouge">gtest</code> targets:</p> <pre><code class="language-CMake">target_link_libraries(${target} PRIVATE GTest::gtest_main)
target_link_libraries(${target} PRIVATE GTest::gmock_main)

gtest_discover_tests(${target})
</code></pre> <p>However, this solution has one drawback that we might only realize very late in the process. The issue is clearly visible in the log before:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">--</span> Installing: /install_process/include/gmock
<span class="nt">--</span> Installing: /install_process/include/gmock/gmock-more-matchers.h
<span class="nt">--</span> Installing: /install_process/include/gmock/internal
<span class="nt">--</span> Installing: /install_process/include/gmock/internal/custom
<span class="nt">--</span> Installing: /install_process/include/gmock/internal/custom/gmock-matchers.h
<span class="nt">--</span> Installing: /install_process/include/gmock/internal/custom/gmock-generated-actions.h
<span class="nt">--</span> Installing: /install_process/include/gmock/internal/custom/gmock-port.h
<span class="nt">--</span> Installing: /install_process/include/gmock/internal/custom/README.md
<span class="nt">--</span> Installing: /install_process/include/gmock/internal/gmock-pp.h
<span class="nt">--</span> Installing: /install_process/include/gmock/internal/gmock-port.h
<span class="nt">--</span> Installing: /install_process/include/gmock/internal/gmock-internal-utils.h
<span class="nt">--</span> Installing: /install_process/include/gmock/gmock-function-mocker.h
<span class="nt">--</span> Installing: /install_process/include/gmock/gmock-spec-builders.h
<span class="nt">--</span> Installing: /install_process/include/gmock/gmock.h
<span class="nt">--</span> Installing: /install_process/include/gmock/gmock-matchers.h
<span class="nt">--</span> Installing: /install_process/include/gmock/gmock-nice-strict.h
<span class="nt">--</span> Installing: /install_process/include/gmock/gmock-cardinalities.h
<span class="nt">--</span> Installing: /install_process/include/gmock/gmock-actions.h
<span class="nt">--</span> Installing: /install_process/include/gmock/gmock-more-actions.h
<span class="nt">--</span> Installing: /install_process/lib/libgmock.a
<span class="nt">--</span> Installing: /install_process/lib/libgmock_main.a
<span class="nt">--</span> Installing: /install_process/lib/pkgconfig/gmock.pc
<span class="nt">--</span> Installing: /install_process/lib/pkgconfig/gmock_main.pc
<span class="nt">--</span> Installing: /install_process/lib/cmake/GTest/GTestTargets.cmake
<span class="nt">--</span> Installing: /install_process/lib/cmake/GTest/GTestTargets-relwithdebinfo.cmake
<span class="nt">--</span> Installing: /install_process/lib/cmake/GTest/GTestConfigVersion.cmake
<span class="nt">--</span> Installing: /install_process/lib/cmake/GTest/GTestConfig.cmake
<span class="nt">--</span> Up-to-date: /install_process/include
<span class="nt">--</span> Installing: /install_process/include/gtest
<span class="nt">--</span> Installing: /install_process/include/gtest/internal
<span class="nt">--</span> Installing: /install_process/include/gtest/internal/gtest-port-arch.h
<span class="nt">--</span> Installing: /install_process/include/gtest/internal/custom
<span class="nt">--</span> Installing: /install_process/include/gtest/internal/custom/gtest-port.h
<span class="nt">--</span> Installing: /install_process/include/gtest/internal/custom/gtest-printers.h
<span class="nt">--</span> Installing: /install_process/include/gtest/internal/custom/README.md
<span class="nt">--</span> Installing: /install_process/include/gtest/internal/custom/gtest.h
<span class="nt">--</span> Installing: /install_process/include/gtest/internal/gtest-death-test-internal.h
<span class="nt">--</span> Installing: /install_process/include/gtest/internal/gtest-internal.h
<span class="nt">--</span> Installing: /install_process/include/gtest/internal/gtest-port.h
<span class="nt">--</span> Installing: /install_process/include/gtest/internal/gtest-filepath.h
<span class="nt">--</span> Installing: /install_process/include/gtest/internal/gtest-param-util.h
<span class="nt">--</span> Installing: /install_process/include/gtest/internal/gtest-type-util.h
<span class="nt">--</span> Installing: /install_process/include/gtest/internal/gtest-string.h
<span class="nt">--</span> Installing: /install_process/include/gtest/gtest-spi.h
<span class="nt">--</span> Installing: /install_process/include/gtest/gtest-typed-test.h
<span class="nt">--</span> Installing: /install_process/include/gtest/gtest-matchers.h
<span class="nt">--</span> Installing: /install_process/include/gtest/gtest-test-part.h
<span class="nt">--</span> Installing: /install_process/include/gtest/gtest-death-test.h
<span class="nt">--</span> Installing: /install_process/include/gtest/gtest-message.h
<span class="nt">--</span> Installing: /install_process/include/gtest/gtest_prod.h
<span class="nt">--</span> Installing: /install_process/include/gtest/gtest-printers.h
<span class="nt">--</span> Installing: /install_process/include/gtest/gtest_pred_impl.h
<span class="nt">--</span> Installing: /install_process/include/gtest/gtest-param-test.h
<span class="nt">--</span> Installing: /install_process/include/gtest/gtest.h
<span class="nt">--</span> Installing: /install_process/lib/libgtest.a
<span class="nt">--</span> Installing: /install_process/lib/libgtest_main.a
<span class="nt">--</span> Installing: /install_process/lib/pkgconfig/gtest.pc
<span class="nt">--</span> Installing: /install_process/lib/pkgconfig/gtest_main.pc
</code></pre></div></div> <p>CMake automatically installed targets imported in the <code class="language-plaintext highlighter-rouge">gtest</code> dependency. It doesn’t matter if we add installation targets very carefully and ensure that the <code class="language-plaintext highlighter-rouge">gtest</code> is not selected for installation. This configuration is very problematic for many C/C++ projects, as there are many situations where we don’t want to install some targets:</p> <ul> <li><strong>Static libraries</strong> are usually already linked to our targets. There is no need to install them.</li> <li><strong>Header-only libraries</strong> are very common in the C++ world. If they are used only internally in translation units and are not exposed to the end user through headers, they should not be installed.</li> <li>While <strong>shared libraries</strong> are usually necessary during deployment, they are sometimes not. For example, serverless functions execute in a dedicated runtime with a pre-defined environment. Building functions might require linking against certain dependencies, e.g., cloud provider’s SDK for databases. This library might already be available at the deployment site.</li> <li><strong>Internal</strong> components, such as testing frameworks, should not be installed or deployed at all.</li> </ul> <p>Contrary to the older solution of using <a href="https://cmake.org/cmake/help/latest/module/ExternalProject.html"><code class="language-plaintext highlighter-rouge">ExternalProject</code></a>, <code class="language-plaintext highlighter-rouge">FetchContent</code> functions do not allow us to disable installation by overriding the installation command with an empty string:</p> <blockquote> <p>ExternalProject_Add: The default install step builds the install target of the external project, but this can be overridden with a custom command using this option (generator expressions are supported). Passing an empty string as the <cmd> makes the install step do nothing.</cmd></p> </blockquote> <p>Fortunately, there is an easy solution, but it requires a little more work than just using <code class="language-plaintext highlighter-rouge">FetchContent</code>. The call to <code class="language-plaintext highlighter-rouge">FetchContent_MakeAvailable</code> populates the dependency, if this has not already been done, and adds its targets to the main configuration. We can replace this call with a slightly more verbose solution:</p> <div class="language-cmake highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">FetchContent_GetProperties</span><span class="p">(</span>googletest<span class="p">)</span>
<span class="nb">if</span><span class="p">(</span>NOT googletest_POPULATED<span class="p">)</span>
  <span class="nf">FetchContent_Populate</span><span class="p">(</span>googletest<span class="p">)</span>
  <span class="nb">add_subdirectory</span><span class="p">(</span><span class="si">${</span><span class="nv">googletest_SOURCE_DIR</span><span class="si">}</span> <span class="si">${</span><span class="nv">googletest_BINARY_DIR</span><span class="si">}</span> EXCLUDE_FROM_ALL<span class="p">)</span>
<span class="nb">endif</span><span class="p">()</span>
</code></pre></div></div> <p>Now, we can use the <code class="language-plaintext highlighter-rouge">EXCLUDE_FROM_ALL</code> flag when manually adding the dependency to the project. Thus, we can still use those targets as previously, but they will not appear in our installation - unless we add them there explicitly.</p>]]></content><author><name></name></author><category term="c++"/><category term="c++"/><category term="cmake"/><summary type="html"><![CDATA[The FetchContent module is the easiest and most efficient way of adding dependencies to your CMake project. In contrast to ExternalProject, it fetches the dependency at the configuration time, which makes it easier to discover imported targets and verify that you are linking your executables and libraries correctly. Thus, you can now easily add to your C/C++ project dependencies that you do not expect to be available on the user’s system - libraries, SDKs, and internal tools such as testing frameworks. In fact, the first example shown in CMake documentation uses the gtest framework:]]></summary></entry><entry><title type="html">Debugging the debugger</title><link href="https://mcopik.github.io/blog/2022/gdb-bug/" rel="alternate" type="text/html" title="Debugging the debugger"/><published>2022-09-19T08:00:00+00:00</published><updated>2022-09-19T08:00:00+00:00</updated><id>https://mcopik.github.io/blog/2022/gdb-bug</id><content type="html" xml:base="https://mcopik.github.io/blog/2022/gdb-bug/"><![CDATA[<p>While working on our high-performance serverless platform <a href="#/projects/rfaas">rFaaS</a>, I stumbled upon a curious bug in the GNU debugger <code class="language-plaintext highlighter-rouge">gdb</code>. In <code class="language-plaintext highlighter-rouge">rFaaS</code>, we allowed clients to submit the function code directly to a remote executor by shipping the contents of the shared library across networks. This works quite well for simple functions in homogenous HPC clusters since there are no issues with binary compatibility.</p> <p>Sometimes, the function does not execute correctly even if it works when called directly from the application. There might be a bug in serializing function arguments, deserializing binary data in a function, or maybe an unnoticed dependency on the shared state. The easiest way to debug the issue or an unexpected crash is to use the debugger. However, in this case, we observed a very unusual behavior - <strong>gdb always hangs!</strong> Thus, we’re going to look at how to reproduce this issue and find a possible cause.</p> <p>Since <code class="language-plaintext highlighter-rouge">rFaaS</code> spawns new processes to execute the function, we attached <code class="language-plaintext highlighter-rouge">gdb</code> to the process running function. However, this step is unnecessary to reproduce the issue, and we will skip the attaching from this point on.</p> <p>Let’s take a look at the following code that reads a shared library, extracts a function <code class="language-plaintext highlighter-rouge">foo</code> that accepts a single integer and returns an integer, and executes the function.</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kt">void</span><span class="o">*</span> <span class="n">library_handle</span> <span class="o">=</span> <span class="n">dlopen</span><span class="p">(</span><span class="s">"./lib.so"</span><span class="p">,</span> <span class="n">RTLD_NOW</span><span class="p">);</span>
<span class="n">assert</span><span class="p">(</span><span class="n">library_handle</span><span class="p">);</span>

<span class="k">typedef</span> <span class="nf">int</span> <span class="p">(</span><span class="o">*</span><span class="n">func_t</span><span class="p">)(</span><span class="kt">int</span><span class="p">);</span>
<span class="n">func_t</span> <span class="n">func</span> <span class="o">=</span> <span class="n">dlsym</span><span class="p">(</span><span class="n">library_handle</span><span class="p">,</span> <span class="s">"foo"</span><span class="p">);</span>
<span class="n">assert</span><span class="p">(</span><span class="n">func</span><span class="p">);</span>

<span class="n">func</span><span class="p">(</span><span class="mi">42</span><span class="p">);</span>

<span class="n">dlclose</span><span class="p">(</span><span class="n">library_handle</span><span class="p">);</span>
</code></pre></div></div> <p>When the shared library is transmitted over the network, it would be wasteful to write the contents to the file and read it again. Instead, we can create a memory-mapped file and store the data there. In <code class="language-plaintext highlighter-rouge">rFaaS</code>, the data is transmitted over RDMA to the memory location. For simplicity, here we replace it by moving the data from the file to the memory buffer.</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">const</span> <span class="kt">char</span><span class="o">*</span> <span class="n">path</span> <span class="o">=</span> <span class="s">"lib.so"</span><span class="p">;</span>
<span class="c1">// Receive code information</span>
<span class="kt">FILE</span><span class="o">*</span> <span class="n">file</span> <span class="o">=</span> <span class="n">fopen</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="s">"rb"</span><span class="p">);</span>
<span class="n">assert</span><span class="p">(</span><span class="n">file</span><span class="p">);</span>
<span class="n">fseek</span> <span class="p">(</span><span class="n">file</span><span class="p">,</span> <span class="mi">0</span> <span class="p">,</span> <span class="n">SEEK_END</span><span class="p">);</span>
<span class="kt">size_t</span> <span class="n">size</span> <span class="o">=</span> <span class="n">ftell</span><span class="p">(</span><span class="n">file</span><span class="p">);</span>
<span class="n">rewind</span><span class="p">(</span><span class="n">file</span><span class="p">);</span>

<span class="kt">int</span> <span class="n">fd</span> <span class="o">=</span> <span class="n">memfd_create</span><span class="p">(</span><span class="s">"libfunction"</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
<span class="n">assert</span><span class="p">(</span><span class="n">fd</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">);</span>
<span class="kt">int</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">ftruncate</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="n">size</span><span class="p">)</span> <span class="p">;</span>
<span class="n">assert</span><span class="p">(</span><span class="n">ret</span> <span class="o">==</span> <span class="mi">0</span><span class="p">);</span>

<span class="kt">void</span><span class="o">*</span> <span class="n">memory_handle</span> <span class="o">=</span> <span class="n">mmap</span><span class="p">(</span><span class="nb">NULL</span><span class="p">,</span> <span class="n">size</span><span class="p">,</span> <span class="n">PROT_WRITE</span><span class="p">,</span> <span class="n">MAP_SHARED</span><span class="p">,</span> <span class="n">fd</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
<span class="n">assert</span><span class="p">(</span><span class="n">memory_handle</span><span class="p">);</span>
<span class="kt">size_t</span> <span class="n">bytes_read</span> <span class="o">=</span> <span class="n">fread</span><span class="p">(</span><span class="n">memory_handle</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">size</span><span class="p">,</span> <span class="n">file</span><span class="p">);</span>
<span class="n">assert</span><span class="p">(</span><span class="n">bytes_read</span> <span class="o">==</span> <span class="n">size</span><span class="p">);</span>
<span class="n">fclose</span><span class="p">(</span><span class="n">file</span><span class="p">);</span>

<span class="kt">char</span> <span class="n">buf</span><span class="p">[</span><span class="mi">32</span><span class="p">];</span>
<span class="n">snprintf</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="mi">32</span><span class="p">,</span> <span class="s">"%s%d"</span><span class="p">,</span> <span class="s">"/proc/self/fd/"</span><span class="p">,</span> <span class="n">fd</span><span class="p">);</span>
<span class="kt">void</span><span class="o">*</span> <span class="n">library_handle</span> <span class="o">=</span> <span class="n">dlopen</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="n">RTLD_NOW</span><span class="p">);</span>
</code></pre></div></div> <p>This code works fine as well - the <code class="language-plaintext highlighter-rouge">library_handle</code> can be used identically as in the previous code snippet.</p> <p>Let’s assume that our function experiences some issues, and we want to find the root cause. For a spawned process, we could insert an artificial loop waiting on a test variable, attach the debugger to the process, set up breakpoints as desired, and change the variable value to continue execution. To simplify the discussion, we will execute the code directly from the main application and skip the process spawn.</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ gdb ./from_memory 
GNU gdb <span class="o">(</span>Ubuntu 12.0.90-0ubuntu1<span class="o">)</span> 12.0.90
Copyright <span class="o">(</span>C<span class="o">)</span> 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later &lt;http://gnu.org/licenses/gpl.html&gt;
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type <span class="s2">"show copying"</span> and <span class="s2">"show warranty"</span> <span class="k">for </span>details.
This GDB was configured as <span class="s2">"x86_64-linux-gnu"</span><span class="nb">.</span>
Type <span class="s2">"show configuration"</span> <span class="k">for </span>configuration details.
For bug reporting instructions, please see:
&lt;https://www.gnu.org/software/gdb/bugs/&gt;.
Find the GDB manual and other documentation resources online at:
    &lt;http://www.gnu.org/software/gdb/documentation/&gt;.

For <span class="nb">help</span>, <span class="nb">type</span> <span class="s2">"help"</span><span class="nb">.</span>
Type <span class="s2">"apropos word"</span> to search <span class="k">for </span>commands related to <span class="s2">"word"</span>...
Reading symbols from ./from_memory...
<span class="o">(</span>gdb<span class="o">)</span> r
Starting program: /home/mcopik/bug_report/from_memory 
<span class="o">[</span>Thread debugging using libthread_db enabled]
Using host libthread_db library <span class="s2">"/lib/x86_64-linux-gnu/libthread_db.so.1"</span><span class="nb">.</span>
</code></pre></div></div> <p>Here we observe the issue - <code class="language-plaintext highlighter-rouge">gdb</code> hangs. It does not respond to any comments, it does not respond to signals, and we need to use <code class="language-plaintext highlighter-rouge">SIGKILL</code> to terminate the debugging session. But what can be going wrong in this example? Can it be caused just by our memory-mapped files? Let’s attach <code class="language-plaintext highlighter-rouge">gdb</code> to the frozen <code class="language-plaintext highlighter-rouge">gdb</code> instance to find what might be happening.</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Attaching to process 546764
<span class="o">[</span>New LWP 546767]
<span class="o">[</span>New LWP 546768]
<span class="o">[</span>New LWP 546769]
<span class="o">[</span>New LWP 546770]
<span class="o">[</span>New LWP 546771]
<span class="o">[</span>New LWP 546772]
<span class="o">[</span>New LWP 546773]
<span class="o">[</span>New LWP 546774]
<span class="o">[</span>Thread debugging using libthread_db enabled]
Using host libthread_db library <span class="s2">"/lib/x86_64-linux-gnu/libthread_db.so.1"</span><span class="nb">.</span>
__GI___libc_read <span class="o">(</span><span class="nv">nbytes</span><span class="o">=</span>4096, <span class="nv">buf</span><span class="o">=</span>0x564e51949e30, <span class="nv">fd</span><span class="o">=</span>15<span class="o">)</span> at ../sysdeps/unix/sysv/linux/read.c:26
26      ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
<span class="o">(</span>gdb<span class="o">)</span> bt
<span class="c">#0  __GI___libc_read (nbytes=4096, buf=0x564e51949e30, fd=15) at ../sysdeps/unix/sysv/linux/read.c:26</span>
<span class="c">#1  __GI___libc_read (fd=15, buf=0x564e51949e30, nbytes=4096) at ../sysdeps/unix/sysv/linux/read.c:24</span>
<span class="c">#2  0x00007f69046c3cb6 in _IO_new_file_underflow (fp=0x564e517345d0) at ./libio/libioP.h:947</span>
<span class="c">#3  0x00007f69046c24b8 in __GI__IO_file_xsgetn (fp=0x564e517345d0, data=&lt;optimized out&gt;, n=64) at ./libio/fileops.c:1321</span>
<span class="c">#4  0x00007f69046b6c29 in __GI__IO_fread (buf=0x7ffc6e69b6f0, size=1, count=64, fp=0x564e517345d0) at ./libio/iofread.c:38</span>
<span class="c">#5  0x0000564e4f0c833e in ?? ()</span>
<span class="c">#6  0x0000564e4f0c842a in ?? ()</span>
<span class="c">#7  0x0000564e4f0c7224 in ?? ()</span>
<span class="c">#8  0x0000564e4f0f338b in ?? ()</span>
<span class="c">#9  0x0000564e4f0cc08a in ?? ()</span>
<span class="c">#10 0x0000564e4f0cb946 in ?? ()</span>
<span class="c">#11 0x0000564e4efb9bae in ?? ()</span>
<span class="c">#12 0x0000564e4efb8c97 in ?? ()</span>
<span class="c">#13 0x0000564e4efbac77 in ?? ()</span>
<span class="c">#14 0x0000564e4efbb6cb in ?? ()</span>
<span class="c">#15 0x0000564e4efbb923 in ?? ()</span>
<span class="c">#16 0x0000564e4ecf1cc5 in ?? ()</span>
<span class="c">#17 0x0000564e4ee79d96 in ?? ()</span>
<span class="c">#18 0x0000564e4ee7bba3 in ?? ()</span>
<span class="c">#19 0x0000564e4ee7d5c1 in ?? ()</span>
<span class="c">#20 0x0000564e4f1b6576 in ?? ()</span>
<span class="c">#21 0x0000564e4f1b6a5a in ?? ()</span>
<span class="c">#22 0x0000564e4eec227d in ?? ()</span>
<span class="c">#23 0x0000564e4eec3f65 in ?? ()</span>
<span class="c">#24 0x0000564e4ec5a150 in ?? ()</span>
<span class="c">#25 0x00007f6904660d90 in __libc_start_call_main (main=main@entry=0x564e4ec5a110, argc=argc@entry=2, argv=argv@entry=0x7ffc6e69c568)</span>
    at ../sysdeps/nptl/libc_start_call_main.h:58
<span class="c">#26 0x00007f6904660e40 in __libc_start_main_impl (main=0x564e4ec5a110, argc=2, argv=0x7ffc6e69c568, init=&lt;optimized out&gt;, fini=&lt;optimized out&gt;, </span>
    <span class="nv">rtld_fini</span><span class="o">=</span>&lt;optimized out&gt;, <span class="nv">stack_end</span><span class="o">=</span>0x7ffc6e69c558<span class="o">)</span> at ../csu/libc-start.c:392
<span class="c">#27 0x0000564e4ec5fbf5 in ?? ()</span>
</code></pre></div></div> <p>We can see at the stack frame <code class="language-plaintext highlighter-rouge">#4</code> that the issue is in a call that attempts to read some file data. Since I first noticed this issue with <code class="language-plaintext highlighter-rouge">gdb</code> version <code class="language-plaintext highlighter-rouge">12.0.90-0ubuntu1</code>, I manually built the newest version <code class="language-plaintext highlighter-rouge">12.1</code>. The entire process is straightforward: run <code class="language-plaintext highlighter-rouge">./configure</code> and <code class="language-plaintext highlighter-rouge">make -j${CPUS}</code>. The problem persists, but we can now use a debug build of <code class="language-plaintext highlighter-rouge">gdb</code> to get more insight.</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Attaching to process 573765
<span class="o">[</span>New LWP 573767]
<span class="o">[</span>New LWP 573768]
<span class="o">[</span>New LWP 573769]
<span class="o">[</span>New LWP 573770]
<span class="o">[</span>New LWP 573771]
<span class="o">[</span>New LWP 573772]
<span class="o">[</span>New LWP 573773]
<span class="o">[</span>New LWP 573774]
<span class="o">[</span>Thread debugging using libthread_db enabled]
Using host libthread_db library <span class="s2">"/lib/x86_64-linux-gnu/libthread_db.so.1"</span><span class="nb">.</span>
__GI___libc_read <span class="o">(</span><span class="nv">nbytes</span><span class="o">=</span>4096, <span class="nv">buf</span><span class="o">=</span>0x558434f1b220, <span class="nv">fd</span><span class="o">=</span>15<span class="o">)</span> at ../sysdeps/unix/sysv/linux/read.c:26
26      ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
<span class="o">(</span>gdb<span class="o">)</span> bt
<span class="c">#0  __GI___libc_read (nbytes=4096, buf=0x558434f1b220, fd=15) at ../sysdeps/unix/sysv/linux/read.c:26</span>
<span class="c">#1  __GI___libc_read (fd=15, buf=0x558434f1b220, nbytes=4096) at ../sysdeps/unix/sysv/linux/read.c:24</span>
<span class="c">#2  0x00007f82b8697cb6 in _IO_new_file_underflow (fp=0x558434e4f2c0) at ./libio/libioP.h:947</span>
<span class="c">#3  0x00007f82b86964b8 in __GI__IO_file_xsgetn (fp=0x558434e4f2c0, data=&lt;optimized out&gt;, n=64) at ./libio/fileops.c:1321</span>
<span class="c">#4  0x00007f82b868ac29 in __GI__IO_fread (buf=buf@entry=0x7ffc32027bb0, size=size@entry=1, count=count@entry=64, fp=fp@entry=0x558434e4f2c0)</span>
    at ./libio/iofread.c:38
<span class="c">#5  0x000055843364f53e in fread (__stream=0x558434e4f2c0, __n=64, __size=1, __ptr=0x7ffc32027bb0) at /usr/include/x86_64-linux-gnu/bits/stdio2.h:293</span>
<span class="c">#6  cache_bread_1 (nbytes=64, buf=0x7ffc32027bb0, f=0x558434e4f2c0) at cache.c:319</span>
<span class="c">#7  cache_bread (abfd=&lt;optimized out&gt;, buf=0x7ffc32027bb0, nbytes=64) at cache.c:358</span>
<span class="c">#8  0x000055843364e564 in bfd_bread (ptr=ptr@entry=0x7ffc32027bb0, size=&lt;optimized out&gt;, size@entry=64, abfd=&lt;optimized out&gt;, abfd@entry=0x558434f0f210)</span>
    at bfdio.c:259
<span class="c">#9  0x000055843366ca53 in bfd_elf64_object_p (abfd=0x558434f0f210) at /home/mcopik/bug_report/build/gdb-12.1/bfd/elfcode.h:519</span>
<span class="c">#10 0x000055843365199c in bfd_check_format_matches (abfd=0x558434f0f210, format=&lt;optimized out&gt;, matching=0x0) at format.c:344</span>
<span class="c">#11 0x000055843351edfe in solib_bfd_open (pathname=0x558434e90200 "/proc/self/fd/4") at ./../gdbsupport/gdb_ref_ptr.h:130</span>
<span class="c">#12 0x000055843351dee7 in solib_map_sections (so=0x558434f0ff00) at solib.c:540</span>
<span class="c">#13 0x000055843351fe56 in update_solib_list (from_tty=&lt;optimized out&gt;) at solib.c:860</span>
<span class="c">#14 0x0000558433520877 in solib_add (pattern=pattern@entry=0x0, from_tty=from_tty@entry=0, readsyms=1) at solib.c:960</span>
<span class="c">#15 0x0000558433520b00 in handle_solib_event () at solib.c:1269</span>
<span class="c">#16 0x0000558433252165 in bpstat_stop_status (aspace=&lt;optimized out&gt;, bp_addr=bp_addr@entry=140737353900800, thread=thread@entry=0x558434ddf090, ws=..., </span>
    <span class="nv">stop_chain</span><span class="o">=</span>stop_chain@entry<span class="o">=</span>0x0<span class="o">)</span> at breakpoint.c:5455
<span class="c">#17 0x00005584333dbc8b in handle_signal_stop (ecs=0x7ffc32028700) at infrun.c:6191</span>
<span class="c">#18 0x00005584333dda68 in handle_stop_requested (ecs=&lt;optimized out&gt;) at infrun.c:4465</span>
<span class="c">#19 handle_stop_requested (ecs=&lt;optimized out&gt;) at infrun.c:4460</span>
<span class="c">#20 handle_inferior_event (ecs=0x7ffc32028700) at infrun.c:5695</span>
<span class="c">#21 0x00005584333df48e in fetch_inferior_event () at infrun.c:4085</span>
<span class="c">#22 0x00005584336f7ef6 in gdb_wait_for_event (block=block@entry=0) at event-loop.cc:700</span>
<span class="c">#23 0x00005584336f83da in gdb_wait_for_event (block=0) at event-loop.cc:596</span>
<span class="c">#24 gdb_do_one_event () at event-loop.cc:212</span>
<span class="c">#25 0x0000558433424275 in start_event_loop () at main.c:421</span>
<span class="c">#26 captured_command_loop () at main.c:481</span>
<span class="c">#27 0x0000558433425e75 in captured_main (data=0x7ffc320288a0) at main.c:1351</span>
<span class="c">#28 gdb_main (args=args@entry=0x7ffc320288d0) at main.c:1366</span>
<span class="c">#29 0x00005584331b9d10 in main (argc=&lt;optimized out&gt;, argv=&lt;optimized out&gt;) at gdb.c:32</span>
</code></pre></div></div> <p>The stackframe <code class="language-plaintext highlighter-rouge">#11</code> proves that <code class="language-plaintext highlighter-rouge">gdb</code> is trying to open a filedescriptor associated with our memory-mapped file. Then, the request is redirected to its internal cache of file descriptors at stackframes <code class="language-plaintext highlighter-rouge">#6</code> and <code class="language-plaintext highlighter-rouge">#7</code>, where a single call to <code class="language-plaintext highlighter-rouge">fread</code> is made. The function <code class="language-plaintext highlighter-rouge">cache_bread_1</code> attempts to read 64 bytes from the file, which does not terminate.</p> <p>What if <code class="language-plaintext highlighter-rouge">gdb</code> cannot read the data because it’s simply not there? We might have to flush the data back to the filesystem to make it visible to other processes.</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">msync</span><span class="p">(</span><span class="n">memory_handle</span><span class="p">,</span> <span class="n">size</span><span class="p">,</span> <span class="n">MS_SYNC</span><span class="p">);</span>
</code></pre></div></div> <p>Unfortunately, this does not resolve the issue. The problem seems to be a core <code class="language-plaintext highlighter-rouge">gdb</code> issue that I cannot resolve by myself, and I opened a <a href="https://sourceware.org/bugzilla/show_bug.cgi?id=29586">bug request at their Bugzilla</a>.</p> <p>You can find all of the code and compilation scripts on <a href="https://gist.github.com/mcopik/c6dc64e6b24aea9576d517ca00d1a9c0">GitHub</a>.</p>]]></content><author><name></name></author><category term="linux"/><category term="linux"/><category term="c"/><summary type="html"><![CDATA[While working on our high-performance serverless platform rFaaS, I stumbled upon a curious bug in the GNU debugger gdb. In rFaaS, we allowed clients to submit the function code directly to a remote executor by shipping the contents of the shared library across networks. This works quite well for simple functions in homogenous HPC clusters since there are no issues with binary compatibility.]]></summary></entry><entry><title type="html">Remote Bash scripts with SSH</title><link href="https://mcopik.github.io/blog/2022/bash-remote/" rel="alternate" type="text/html" title="Remote Bash scripts with SSH"/><published>2022-09-01T08:00:00+00:00</published><updated>2022-09-01T08:00:00+00:00</updated><id>https://mcopik.github.io/blog/2022/bash-remote</id><content type="html" xml:base="https://mcopik.github.io/blog/2022/bash-remote/"><![CDATA[<p>What is the easiest way of executing a Bash script on remote machines, with support for arbitrary options and arguments, without having to transmit the code manually? Let’s say we have a script to invoke that we want to run on a remote machine - in our case, it’s the part of <a href="#/projects/rfaas">rFaaS</a> that scans network interfaces and RDMA links to generate a device database.</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ tools/device_generator.sh <span class="nt">-d</span> wlp61s0 <span class="nt">-o</span> devices.json
</code></pre></div></div> <p>We want to execute this command on many endpoints in parallel since they will be used to host rFaaS executors. While this is trivial in a supercomputer where all nodes can read the script from a shared parallel filesystem, it is not so simple in the cloud, where virtual machines might not have common storage. Fortunately, we don’t have to copy the code manually - we can feed bash standard input with the script contents:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ ssh <span class="nv">$node</span> bash &lt; tools/device_generator.sh
</code></pre></div></div> <p>But what if we want to pass positional arguments? That’s why we need the <code class="language-plaintext highlighter-rouge">-s</code> flag:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ ssh <span class="nv">$node</span> bash <span class="nt">-s</span> <span class="nv">$ARG</span> &lt; tools/device_generator.sh
</code></pre></div></div> <p>But what if we want to support options in our script? We can’t put them here since bash will interpret it as its own option. So instead, we need a double dash (<code class="language-plaintext highlighter-rouge">--</code>) to mark the end of bash options and the start of positional arguments:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ ssh <span class="nv">$node</span> bash <span class="nt">-s</span> <span class="nt">--</span> <span class="nt">-d</span> wlp61s0 <span class="nv">$ARG</span> &lt; tools/device_generator.sh <span class="o">&gt;</span> <span class="nv">$node</span>.json
</code></pre></div></div> <p>This command can be easily combined with forked processes running in the background, scaling the query to dozens and even hundreds of machines in parallel.</p> <p>And what if we are in a supercomputer? Then we do not need such tricks at all. For example, if we have already allocated a bunch of nodes with SLURM’s command <code class="language-plaintext highlighter-rouge">salloc</code>, then we just need to use:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ srun /bin/bash <span class="nt">-c</span> <span class="s1">'tools/device_generator.sh &gt; $(hostname -s).json'</span>
</code></pre></div></div> <p>Using single quotes prevents variable expansion on the host side. Thus, we will obtain one file for each node in the allocation.</p>]]></content><author><name></name></author><category term="linux"/><category term="linux"/><category term="cli"/><category term="tips"/><summary type="html"><![CDATA[What is the easiest way of executing a Bash script on remote machines, with support for arbitrary options and arguments, without having to transmit the code manually? Let’s say we have a script to invoke that we want to run on a remote machine - in our case, it’s the part of rFaaS that scans network interfaces and RDMA links to generate a device database.]]></summary></entry><entry><title type="html">JSON in Bash and CLI with jq</title><link href="https://mcopik.github.io/blog/2021/jq/" rel="alternate" type="text/html" title="JSON in Bash and CLI with jq"/><published>2021-07-30T08:00:00+00:00</published><updated>2021-07-30T08:00:00+00:00</updated><id>https://mcopik.github.io/blog/2021/jq</id><content type="html" xml:base="https://mcopik.github.io/blog/2021/jq/"><![CDATA[<p>The JSON data format has become a ubiquitous tool for interchanging and storing human-readable data. In particular, it is very convenient when it comes to storing user-defined settings and properties. For example, in rFaaS, <a href="/projects/rfaas">our RDMA-accelerated serverless platform</a>, we have to store multi-parameter device configurations for the local and remote endpoints. Thus, we want to have the configuration in JSON as this format is flexible, widely supported, and easy to parse for humans:</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">"devices"</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><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"rocep7s0"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"ip_address"</span><span class="p">:</span><span class="w"> </span><span class="s2">"192.168.0.18"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"port"</span><span class="p">:</span><span class="w"> </span><span class="mi">10005</span><span class="p">,</span><span class="w">
      </span><span class="nl">"max_inline_data"</span><span class="p">:</span><span class="w"> </span><span class="mi">128</span><span class="p">,</span><span class="w">
      </span><span class="nl">"default_receive_buffer_size"</span><span class="p">:</span><span class="w"> </span><span class="mi">32</span><span class="w">
    </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">"rocep6s0f0"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"ip_address"</span><span class="p">:</span><span class="w"> </span><span class="s2">"192.168.1.31"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"port"</span><span class="p">:</span><span class="w"> </span><span class="mi">10000</span><span class="p">,</span><span class="w">
      </span><span class="nl">"max_inline_data"</span><span class="p">:</span><span class="w"> </span><span class="mi">128</span><span class="p">,</span><span class="w">
      </span><span class="nl">"default_receive_buffer_size"</span><span class="p">:</span><span class="w"> </span><span class="mi">32</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>While JSON is natively supported in many languages, and even C++ has a multitude of easy-to-use or high-performance JSON libraries, there’s no native way of using JSONs in Bash scripts. Bash syntax and capabilities are a bit ancient, but automated testing and benchmarking scripts could benefit from using the same configuration format. Fortunately, there’s an easy and lightweight tool for that - <a href="https://stedolan.github.io/jq/">jq</a>. <code class="language-plaintext highlighter-rouge">jq</code> is a very simple querying tool that allows reading and processing all complex and nested JSON structures. Below, I will present a selection of queries in <code class="language-plaintext highlighter-rouge">jq</code> that demonstrate the basic usage and capabilities of this simple yet powerful and useful tool.</p> <p>The simplest query displays all JSON contents.</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ jq <span class="s1">'.'</span> configuration/devices.json
</code></pre></div></div> <p>Another one can be used to create a JSON object from scratch.</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ jq <span class="nt">--null-input</span> <span class="s1">'{}'</span>
</code></pre></div></div> <p>Selecting JSON object.</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ jq <span class="s1">'.["devices"]'</span> configuration/devices.json
<span class="o">[</span>
  <span class="o">{</span>
    <span class="s2">"name"</span>: <span class="s2">"rocep7s0"</span>,
    <span class="s2">"ip_address"</span>: <span class="s2">"192.168.0.18"</span>,
    <span class="s2">"port"</span>: 10005,
    <span class="s2">"max_inline_data"</span>: 128,
    <span class="s2">"default_receive_buffer_size"</span>: 32
  <span class="o">}</span>,
  <span class="o">{</span>
    <span class="s2">"name"</span>: <span class="s2">"rocep6s0f0"</span>,
    <span class="s2">"ip_address"</span>: <span class="s2">"192.168.1.31"</span>,
    <span class="s2">"port"</span>: 10000,
    <span class="s2">"max_inline_data"</span>: 128,
    <span class="s2">"default_receive_buffer_size"</span>: 32
  <span class="o">}</span>
<span class="o">]</span>
</code></pre></div></div> <p>Selecting object in an array through direct indexing.</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ jq <span class="s1">'.["devices"][0]'</span> configuration/devices.json
<span class="o">{</span>
  <span class="s2">"name"</span>: <span class="s2">"rocep7s0"</span>,
  <span class="s2">"ip_address"</span>: <span class="s2">"192.168.0.18"</span>,
  <span class="s2">"port"</span>: 10005,
  <span class="s2">"max_inline_data"</span>: 128,
  <span class="s2">"default_receive_buffer_size"</span>: 32
<span class="o">}</span>
</code></pre></div></div> <p>Requesting a specific field in the object.</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ jq <span class="s1">'.["devices"][0]["ip_address"]'</span> configuration/devices.json
<span class="s2">"192.168.0.18"</span>
</code></pre></div></div> <p>The quotation marks are usually not needed but we don’t need <code class="language-plaintext highlighter-rouge">sed</code> or <code class="language-plaintext highlighter-rouge">tr</code> to remove them.</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ jq <span class="nt">-r</span> <span class="s1">'.["devices"][0]["ip_address"]'</span> configuration/devices.json
192.168.0.18
</code></pre></div></div> <p>What if we want to access multiple fields with a single query?</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ jq <span class="nt">-r</span> <span class="s1">'.["devices"][0] | .ip_address,.port'</span> configuration/devices.json
192.168.0.18
10005
</code></pre></div></div> <p>Unnecessary whitespace can be removed too.</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ jq <span class="nt">-j</span> <span class="s1">'.["devices"][0] | .ip_address,.port'</span> configuration/devices.json
192.168.0.1810005
</code></pre></div></div> <p>However, this output is far from ideal. Instead, we might want to get a CSV-like or a semicolon-separated output. We can use string interpolation for that.</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ jq <span class="nt">-j</span> <span class="s1">'.["devices"][0] | "\(.ip_address);\(.port)"'</span> configuration/devices.json
192.168.0.18<span class="p">;</span>10005
</code></pre></div></div> <p>Thus, we can process device addresses with this one-liner in Bash.</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ <span class="nv">IFS</span><span class="o">=</span><span class="s2">";"</span> <span class="nb">read </span>ip port <span class="o">&lt;&lt;&lt;</span> <span class="si">$(</span>jq <span class="nt">-j</span> <span class="s1">'.["devices"][0] | "\(.ip_address);\(.port)"'</span> configuration/devices.json<span class="si">)</span>
~ <span class="nb">echo</span> <span class="nv">$ip</span> <span class="nv">$port</span>
192.168.0.18 10005
</code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">jq</code> is not only about simple queries and text parsing. The tool allows more complex operations, including finding an object with a specific value. In our case, we want to find the address of a device with a given name, without knowing its position in the <code class="language-plaintext highlighter-rouge">devices</code> array:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ jq <span class="nt">-r</span> <span class="s1">'.devices[] | select(.name=="rocep7s0") | .ip_address'</span> configuration/devices.json
192.168.0.18
</code></pre></div></div> <p>Furthermore, objects can be freely modified. In this example, we add a new simple object to the array.</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ jq <span class="s1">'.devices += [{"name": "new_dev"}]'</span> configuration/devices.json
</code></pre></div></div> <p>Do you need to expand your JSON with new values that are defined at runtime? You can tell <code class="language-plaintext highlighter-rouge">jq</code> to treat certain values as arguments:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ jq <span class="nt">--arg</span> addr <span class="nv">$ADDRESS</span> <span class="s1">'.devices[0]["ip_address"] = $addr'</span> configuration/devices.json
</code></pre></div></div> <p>Do you need to merge two JSON files or copy values from one JSON to another? Use the <code class="language-plaintext highlighter-rouge">argfile</code> option. In this example, we add elements of an array from the file <code class="language-plaintext highlighter-rouge">devices.json</code> to the array in <code class="language-plaintext highlighter-rouge">configuration/devices.json</code>.</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ jq <span class="nt">--argfile</span> file1 devices.json <span class="s1">'.devices += $file1.devices'</span> configuration/devices.json
</code></pre></div></div> <p>More advanced queries are available in <code class="language-plaintext highlighter-rouge">jq</code>, including scripting with <a href="https://stedolan.github.io/jq/manual/#Advancedfeatures"><code class="language-plaintext highlighter-rouge">select</code>, <code class="language-plaintext highlighter-rouge">map</code>, and user-defined functions</a>.</p>]]></content><author><name></name></author><category term="linux"/><category term="linux"/><category term="cli"/><category term="tips"/><summary type="html"><![CDATA[The JSON data format has become a ubiquitous tool for interchanging and storing human-readable data. In particular, it is very convenient when it comes to storing user-defined settings and properties. For example, in rFaaS, our RDMA-accelerated serverless platform, we have to store multi-parameter device configurations for the local and remote endpoints. Thus, we want to have the configuration in JSON as this format is flexible, widely supported, and easy to parse for humans:]]></summary></entry><entry><title type="html">Relative paths in LaTeX.</title><link href="https://mcopik.github.io/blog/2021/latex-relative-include/" rel="alternate" type="text/html" title="Relative paths in LaTeX."/><published>2021-07-19T08:00:00+00:00</published><updated>2021-07-19T08:00:00+00:00</updated><id>https://mcopik.github.io/blog/2021/latex-relative-include</id><content type="html" xml:base="https://mcopik.github.io/blog/2021/latex-relative-include/"><![CDATA[<p>Recently, I started to gather the LaTeX tools, scripts, and useful imports in a single repository: <a href="https://github.com/mcopik/latex-tools">latex-tools @ GitHub.com</a>. I designed it to have a single entry point <code class="language-plaintext highlighter-rouge">includes.tex</code>, and for each paper, I could add this repository as a submodule.</p> <p>But how should I include such a file in a LaTeX document? One option is to use <code class="language-plaintext highlighter-rouge">include</code>, but it mustn’t be used in preambles which disqualifies it from handling package imports. Instead, we can use <code class="language-plaintext highlighter-rouge">input</code> that works similarly to the C/C++ <code class="language-plaintext highlighter-rouge">#include</code>, doing a simple copy&amp;paste. It does not simplify the build and development, as <code class="language-plaintext highlighter-rouge">include</code> does by splitting <code class="language-plaintext highlighter-rouge">.tex</code> files into different compilation units, which is great for books and theses with many chapters. However, you can use <code class="language-plaintext highlighter-rouge">input</code> with almost all parts of the document and you are allowed to use nested inputs.</p> <p>There’s one caveat, though: <code class="language-plaintext highlighter-rouge">input</code> does not support relative imports. So, for example, if we have the following directory structure, we won’t be able to use <code class="language-plaintext highlighter-rouge">\usepackage{dependency}</code> in <code class="language-plaintext highlighter-rouge">includes.tex</code>:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+-- paper.tex
+-- latex-tools
|   +-- dependency.sty
|   +-- includes.tex
</code></pre></div></div> <p>The paths in <code class="language-plaintext highlighter-rouge">input</code> are always relative to the main file. Thus, we end up with an error:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>! LaTeX Error: File `dependency.sty' not found.
</code></pre></div></div> <p>Fortunately, there’s an <a href="https://www.ctan.org/pkg/import"><code class="language-plaintext highlighter-rouge">import</code></a> package to the rescue! The last <code class="language-plaintext highlighter-rouge">/</code> in directory name is important, otherwise the path is not correctly processed.</p> <div class="language-latex highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">\usepackage</span><span class="p">{</span>import<span class="p">}</span>
<span class="k">\subimport</span><span class="p">{</span>latex-tools/<span class="p">}{</span>includes.tex<span class="p">}</span>
</code></pre></div></div> <p>Thus, we can easily decompose LaTeX sources, tables, and graphics into a hierarchy of modules.</p>]]></content><author><name></name></author><category term="tips"/><category term="latex"/><summary type="html"><![CDATA[Recently, I started to gather the LaTeX tools, scripts, and useful imports in a single repository: latex-tools @ GitHub.com. I designed it to have a single entry point includes.tex, and for each paper, I could add this repository as a submodule.]]></summary></entry><entry><title type="html">C++ Toolchain with Taint Analysis</title><link href="https://mcopik.github.io/blog/2020/dataflow/" rel="alternate" type="text/html" title="C++ Toolchain with Taint Analysis"/><published>2020-02-24T08:00:00+00:00</published><updated>2020-02-24T08:00:00+00:00</updated><id>https://mcopik.github.io/blog/2020/dataflow</id><content type="html" xml:base="https://mcopik.github.io/blog/2020/dataflow/"><![CDATA[<p>Clang comes with a set of tools known as <em>sanitizers</em> that provide a runtime verification of common problems such as memory issues and undefined behavior. An interesting and a not well-known one is DfSan, a dataflow sanitizer. The name does not really reveal the true purpose: the library brings a compiler pass and runtime to implement <strong>taint analysis</strong><sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>. The main purpose is to <strong>taint</strong> specific memory regions and automatically propagate taint labels to other locations in memory that are affected by originally tainted regions. Taint labels are stored separately, in a so-called shadow memory, and compiler pass instruments codes with taint propagation. Thus, it is possible for every variable in a program to detect which values have affected it. The analysis is not only fully inter-procedural but it is completely memory agnostic, which is a common issue for static compiler analyses. The sanitizer implements <strong>data-flow tainting</strong>, the most common way of propagating labels:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kt">void</span> <span class="nf">f</span><span class="p">(</span><span class="kt">int</span> <span class="o">*</span> <span class="n">x</span><span class="p">)</span>
<span class="p">{</span>
  <span class="c1">// Taint label input_parameter detected in y!</span>
  <span class="kt">int</span> <span class="n">y</span> <span class="o">=</span> <span class="o">*</span><span class="n">x</span><span class="p">;</span>
  <span class="n">do_something_important</span><span class="p">(</span><span class="n">y</span><span class="p">);</span>
<span class="p">}</span>

<span class="kt">int</span> <span class="n">input_parameter</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="n">taint</span><span class="p">(</span><span class="o">&amp;</span><span class="n">input_parameter</span><span class="p">,</span> <span class="s">"input_parameter"</span><span class="p">);</span>
<span class="kt">int</span> <span class="n">x</span> <span class="o">=</span> <span class="n">input_parameter</span> <span class="o">*</span> <span class="mi">10</span><span class="p">;</span>
<span class="n">f</span><span class="p">(</span><span class="o">&amp;</span><span class="n">x</span><span class="p">);</span>
</code></pre></div></div> <p>As a sidenote, we notice that variables can be affected through control-flow decision as well:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="n">m</span><span class="p">,</span> <span class="n">n</span><span class="p">;</span>
<span class="n">taint</span><span class="p">(</span><span class="o">&amp;</span><span class="n">m</span><span class="p">,</span> <span class="s">"m"</span><span class="p">);</span>
<span class="n">taint</span><span class="p">(</span><span class="o">&amp;</span><span class="n">n</span><span class="p">,</span> <span class="s">"n"</span><span class="p">);</span>
<span class="kt">int</span> <span class="n">sum</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="c1">// sum is m * n</span>
<span class="c1">// sum should have both `m` and `n` as taint labels</span>
<span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">m</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span>
  <span class="n">sum</span> <span class="o">+=</span> <span class="n">n</span><span class="p">;</span>
</code></pre></div></div> <p>The feature of control-flow tainting<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> has not been implemented in DfSan yet.</p> <p>To be fully precise, DfSan needs to instrument all library functions by propagating taint labels across function arguments and return values. As a result, it creates a wrapper function for each function found in the program. When handling functions that cannot be instrumented, e.g. library functions for which only declaration is available, the function will be considered as potentially problematic unless dfsan is notified through an ABI blacklist that this function does not write into user-accessible memory. For such function there can be no taint propagation or the taint label of return value from the function is overapproximated as a combination of taint labels in all input variables.</p> <p>To make sure that we cover everything, we have to instrument all libraries used by the program, including the standard C++ library.</p> <h3 id="goal">Goal</h3> <p>The main goal is to assemble a complete C++ toolchain with a support for dataflow (taint) analysis. According to Clang documentation<sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>, the complete pipeline includes not only a compiler, but a linker, language standard library implementation and a runtime library. First, we’re going to <a href="#build-clang-with-sanitizers">build Clang with compiler runtime</a>. Afterwards, we’re going to use the new build of clang to <a href="#c-standard-library">prepare sanitized build of libc++</a>. Finally, we build libunwind, the library implementing stack unwinding which is necessary for exception handling, and <a href="#complete-toolchain">assemble the toolchain in a Docker image </a> to form a single distribution of a C++ dfsan pipeline. To demonstrate the tool usability for HPC software, we’re going to take a look on builds with <a href="#openmp">OpenMP</a> and <a href="#mpi">MPI</a>.</p> <h3 id="shared-libraries">Shared libraries</h3> <p>Static libraries are prefered for dfsan to avoid issues with memory mappings. However, statical link of libraries might lead to serious issues when a specific library is linked more than once. For example, an application might link both OpenMP and a library already using OpenMP, leading to a scenario where two OpenMP runtimes are present. That could have catastrophic effects on the performance but it’s not something that we should worry about when trying to instrument an application<sup id="fnref:4"><a href="#fn:4" class="footnote" rel="footnote" role="doc-noteref">4</a></sup>.</p> <h3 id="using-dfsan">Using DfSan</h3> <p>Dfsan might produce a huge number of warnings related on uninstrumented functions. It’s quite benefitial to look through the log at least. Although a significant part of libc is uninstrumented and <code class="language-plaintext highlighter-rouge">sqrt</code> computing square root is marked as functional, implying that taint label of return value is infered from labels of input arguments, the cubic root function <code class="language-plaintext highlighter-rouge">cbrt</code> is marked only as <code class="language-plaintext highlighter-rouge">uninstrumented</code>. Afterwards it’s better to disable the output log entirely with the environment variable:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>DFSAN_OPTIONS=warn_unimplemented=0
</code></pre></div></div> <h2 id="build-clang-with-sanitizers">Build Clang with sanitizers</h2> <p>First, we need to get LLVM and clang with the support to sanitizers - we’re going to use dataflow sanitizer (dfsan). There are three ways to achieve that:</p> <ul> <li>get prebuilt packages via APT for Debian-based systems: https://apt.llvm.org/</li> <li>build from sources, starting with cloning the <a href="https://github.com/llvm/llvm-project">repository</a> and switching to branch corresponding to selected release</li> <li>download sources or prebuilt binaries from <a href="http://releases.llvm.org/download.html">sources</a>. The important part is we need to build Clang with compiler-rt since the latter provides sanitizers.</li> </ul> <p>The build itself is quite straightforward as long as all projects are placed together, just like in a clone from directory, or in a common directory i.e. with directories <code class="language-plaintext highlighter-rouge">clang</code> and <code class="language-plaintext highlighter-rouge">compiler-rt</code> along <code class="language-plaintext highlighter-rouge">LLVM</code>. The CMake configuration with sanitizers and in release mode is as follows:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cmake <span class="nt">-DLLVM_TARGETS_TO_BUILD</span><span class="o">=</span><span class="s2">"X86"</span><span class="se">\</span>
  <span class="o">&amp;&amp;</span> <span class="nt">-DLLVM_ENABLE_PROJECTS</span><span class="o">=</span><span class="s2">"clang;compiler-rt"</span><span class="se">\</span>
  <span class="o">&amp;&amp;</span> <span class="nt">-DCMAKE_BUILD_TYPE</span><span class="o">=</span>Release<span class="se">\</span>
  <span class="o">&amp;&amp;</span> <span class="nt">-DCMAKE_INSTALL_PREFIX</span><span class="o">=</span>/path/to/install<span class="se">\</span>
  <span class="o">&amp;&amp;</span> /path/to/llvm
</code></pre></div></div> <p>We can use dataflow sanitizer to propagate taint labels in a dataflow manner! We do a sanity check by running a shortened version of the <a href="https://clang.llvm.org/docs/DataFlowSanitizer.html">simple example from docs</a>:</p> <div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;sanitizer/dfsan_interface.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;assert.h&gt;</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
<span class="p">{</span>
  <span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
  <span class="n">dfsan_label</span> <span class="n">i_label</span> <span class="o">=</span> <span class="n">dfsan_create_label</span><span class="p">(</span><span class="s">"i"</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
  <span class="n">dfsan_set_label</span><span class="p">(</span><span class="n">i_label</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">i</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">i</span><span class="p">));</span>

  <span class="kt">int</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
  <span class="n">dfsan_label</span> <span class="n">j_label</span> <span class="o">=</span> <span class="n">dfsan_create_label</span><span class="p">(</span><span class="s">"j"</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
  <span class="n">dfsan_set_label</span><span class="p">(</span><span class="n">j_label</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">j</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">j</span><span class="p">));</span>

  <span class="kt">int</span> <span class="n">test</span> <span class="o">=</span> <span class="n">i</span> <span class="o">+</span> <span class="n">j</span><span class="p">;</span>
  <span class="n">dfsan_label</span> <span class="n">test_label</span> <span class="o">=</span> <span class="n">dfsan_read_label</span><span class="p">(</span><span class="o">&amp;</span><span class="n">test</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">test</span><span class="p">));</span>
  <span class="n">assert</span><span class="p">(</span><span class="n">dfsan_has_label</span><span class="p">(</span><span class="n">test_label</span><span class="p">,</span> <span class="n">i_label</span><span class="p">));</span>
  <span class="n">assert</span><span class="p">(</span><span class="n">dfsan_has_label</span><span class="p">(</span><span class="n">test_label</span><span class="p">,</span> <span class="n">j_label</span><span class="p">));</span>

  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div> <p>Which can be built with just a single additional compiler flag:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">PATH</span><span class="o">=</span>/install/path/bin:<span class="nv">$PATH</span>
<span class="c"># make sure we use the correct version</span>
which clang++
clang++ <span class="nt">-fsanitize</span><span class="o">=</span>dataflow dfsan_test.cpp <span class="nt">-o</span> dfsan_test.exe
./dfsan_test.exe
</code></pre></div></div> <p>The <strong>Docker image</strong> with the complete build is available as <a href="https://hub.docker.com/repository/docker/mcopik/clang-dfsan/tags"><code class="language-plaintext highlighter-rouge">mcopik/clang-dfsan:clang-${VERSION}</code></a>.</p> <h2 id="c-standard-library">C++ Standard Library</h2> <p>We want to build both <a href="https://libcxx.llvm.org/">libcxx</a> and <a href="https://libcxxabi.llvm.org/">libcxxabi</a> to provide a fully sanitized standard library implementation that can be used by C++ applications. The libraries are easily built as a part of LLVM tree by selectin LLVM projects <code class="language-plaintext highlighter-rouge">libcxx</code> and <code class="language-plaintext highlighter-rouge">libcxxabi</code>, similarly to the previous setup. However, this requires generation of targets for the entire LLVM project and we can’t easily reuse previous build step because compiler flags are now different.</p> <p>Fortunately, the libraries support a standalone build although it is a bit more complicated due to a cyclic dependency: <code class="language-plaintext highlighter-rouge">libcxx</code> requires <code class="language-plaintext highlighter-rouge">libcxxabi</code> whereas <code class="language-plaintext highlighter-rouge">libcxxabi</code> requires existence of <code class="language-plaintext highlighter-rouge">libcxx</code> headers. The most generic approach would be to build <code class="language-plaintext highlighter-rouge">libcxx</code> against a different ABI, such as <code class="language-plaintext highlighter-rouge">libstdc++</code>, build <code class="language-plaintext highlighter-rouge">libcxxabi</code> and then finally rebuild <code class="language-plaintext highlighter-rouge">libcxx</code> with a proper ABI. But we can take a shortcut here since <code class="language-plaintext highlighter-rouge">libcxxabi</code> requires only headers from the other library, and for that reason we don’t need a full build.</p> <p>First, let’s create a static build of <code class="language-plaintext highlighter-rouge">libcxxabi</code></p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cmake <span class="nt">-G</span> <span class="s2">"Ninja"</span><span class="se">\</span>
  <span class="nt">-DCMAKE_BUILD_TYPE</span><span class="o">=</span>MinSizeRel<span class="se">\</span>
  <span class="nt">-DCMAKE_INSTALL_PREFIX</span><span class="o">=</span>/path/to/install<span class="se">\</span>
  <span class="nt">-DCMAKE_C_COMPILER</span><span class="o">=</span>clang<span class="se">\</span>
  <span class="nt">-DCMAKE_CXX_COMPILER</span><span class="o">=</span>clang++<span class="se">\</span>
  <span class="nt">-DCMAKE_C_FLAGS</span><span class="o">=</span><span class="nt">-fsanitize</span><span class="o">=</span>dataflow<span class="se">\</span>
  <span class="nt">-DCMAKE_CXX_FLAGS</span><span class="o">=</span><span class="nt">-fsanitize</span><span class="o">=</span>dataflow<span class="se">\</span>
  <span class="nt">-DLLVM_PATH</span><span class="o">=</span>/path/to/llvm/install<span class="se">\</span>
  <span class="nt">-DLIBCXXABI_ENABLE_SHARED</span><span class="o">=</span>NO<span class="se">\</span>
  <span class="nt">-DLIBCXXABI_LIBCXX_PATH</span><span class="o">=</span>../libcxx<span class="se">\</span>
  ../libcxxabi
</code></pre></div></div> <p>This will create a single static library file <code class="language-plaintext highlighter-rouge">lib/libc++abi.a</code>. Then, we can get a libc++. In addition, we can use the experimental option <code class="language-plaintext highlighter-rouge">LIBCXX_ENABLE_STATIC_ABI_LIBRARY</code> to link the contents of <code class="language-plaintext highlighter-rouge">libcxxabi</code> with our static copy of <code class="language-plaintext highlighter-rouge">libcxx</code>. This simplifies further usage since <code class="language-plaintext highlighter-rouge">libc++abi.a</code> does not have to be specified every time as link-time dependency.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cmake <span class="nt">-G</span> <span class="s2">"Ninja"</span><span class="se">\</span>
  <span class="nt">-DCMAKE_BUILD_TYPE</span><span class="o">=</span>MinSizeRel<span class="se">\</span>
  <span class="nt">-DCMAKE_INSTALL_PREFIX</span><span class="o">=</span>/opt/llvm<span class="se">\</span>
  <span class="nt">-DCMAKE_C_COMPILER</span><span class="o">=</span>clang<span class="se">\</span>
  <span class="nt">-DCMAKE_CXX_COMPILER</span><span class="o">=</span>clang++<span class="se">\</span>
  <span class="nt">-DCMAKE_C_FLAGS</span><span class="o">=</span><span class="nt">-fsanitize</span><span class="o">=</span>dataflow<span class="se">\</span>
  <span class="nt">-DCMAKE_CXX_FLAGS</span><span class="o">=</span><span class="nt">-fsanitize</span><span class="o">=</span>dataflow<span class="se">\</span>
  <span class="nt">-DLIBCXX_ENABLE_SHARED</span><span class="o">=</span>OFF<span class="se">\</span>
  <span class="nt">-DLIBCXX_CXX_ABI</span><span class="o">=</span>libcxxabi<span class="se">\</span>
  <span class="nt">-DLIBCXX_ENABLE_STATIC_ABI_LIBRARY</span><span class="o">=</span>ON<span class="se">\</span>
  <span class="nt">-DLIBCXX_CXX_ABI_INCLUDE_PATHS</span><span class="o">=</span>../libcxxabi/include/<span class="se">\</span>
  <span class="nt">-DLIBCXX_CXX_ABI_LIBRARY_PATH</span><span class="o">=</span>../build_libcxxabi/lib/<span class="se">\</span>
  ../libcxx
</code></pre></div></div> <p>Afterwards we should only <code class="language-plaintext highlighter-rouge">libc++.a</code> in the <code class="language-plaintext highlighter-rouge">${INSTALL}/lib</code> directory. Let’s do a simple sanity check to verify that our new build works correctly:</p> <div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;vector&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;numeric&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;cassert&gt;</span><span class="cp">
</span>
<span class="cp">#include</span> <span class="cpf">&lt;sanitizer/dfsan_interface.h&gt;</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
<span class="p">{</span>

  <span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
  <span class="n">dfsan_label</span> <span class="n">i_label</span> <span class="o">=</span> <span class="n">dfsan_create_label</span><span class="p">(</span><span class="s">"i"</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
  <span class="n">dfsan_set_label</span><span class="p">(</span><span class="n">i_label</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">i</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">i</span><span class="p">));</span>

  <span class="kt">int</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
  <span class="n">dfsan_label</span> <span class="n">j_label</span> <span class="o">=</span> <span class="n">dfsan_create_label</span><span class="p">(</span><span class="s">"j"</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
  <span class="n">dfsan_set_label</span><span class="p">(</span><span class="n">j_label</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">j</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">j</span><span class="p">));</span>

  <span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span> <span class="n">vec</span><span class="p">{</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="n">i</span><span class="p">};</span>
  <span class="n">dfsan_label</span> <span class="n">test_label</span> <span class="o">=</span> <span class="n">dfsan_read_label</span><span class="p">(</span><span class="o">&amp;</span><span class="n">vec</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="mi">2</span><span class="p">),</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">int</span><span class="p">));</span>
  <span class="n">assert</span><span class="p">(</span><span class="n">dfsan_has_label</span><span class="p">(</span><span class="n">test_label</span><span class="p">,</span> <span class="n">i_label</span><span class="p">));</span>

  <span class="kt">int</span> <span class="n">test</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">accumulate</span><span class="p">(</span><span class="n">vec</span><span class="p">.</span><span class="n">begin</span><span class="p">(),</span> <span class="n">vec</span><span class="p">.</span><span class="n">end</span><span class="p">(),</span> <span class="n">j</span><span class="p">);</span>
  <span class="n">test_label</span> <span class="o">=</span> <span class="n">dfsan_read_label</span><span class="p">(</span><span class="o">&amp;</span><span class="n">test</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">test</span><span class="p">));</span>
  <span class="n">assert</span><span class="p">(</span><span class="n">dfsan_has_label</span><span class="p">(</span><span class="n">test_label</span><span class="p">,</span> <span class="n">i_label</span><span class="p">));</span>
  <span class="n">assert</span><span class="p">(</span><span class="n">dfsan_has_label</span><span class="p">(</span><span class="n">test_label</span><span class="p">,</span> <span class="n">j_label</span><span class="p">));</span>

  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div> <p>To build this, we need to specify in clang arguments that <code class="language-plaintext highlighter-rouge">libc++</code> should be used. Furthermore, we need to provide include and library directories to libc++, unless the standard library was installed in the same directory tree as clang. In such case, the compiler will be able to pick up paths automatically. The link with <code class="language-plaintext highlighter-rouge">libc++abi</code> is unnecesary when libraries were merged in the last build step.</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>clang++ <span class="nt">-I</span> /path/to/libc++/installation/include/c++/v1<span class="se">\</span>
  <span class="nt">-fsanitize</span><span class="o">=</span>dataflow<span class="se">\</span>
  <span class="nt">-L</span> /path/to/libc++/installation/lib<span class="se">\</span>
  <span class="nt">-Wl</span>,--start-group,-lc++abi<span class="se">\</span>
  <span class="nt">-stdlib</span><span class="o">=</span>libc++ dfsan_test.cpp<span class="se">\</span>
  <span class="nt">-o</span> dfsan_test.exe
</code></pre></div></div> <p>The <strong>Docker image</strong> with the complete build is available as <a href="https://hub.docker.com/repository/docker/mcopik/clang-dfsan/tags">`mcopik/clang-dfsan:libcxx-${VERSION}</a>.</p> <h2 id="complete-toolchain">Complete Toolchain</h2> <p>Finally, we can assemble a complete C++ toolchain. In addition to standard C++ library, the compilation workflow has several dependencies such as linker, runtime library or implementation of exceptions. One could use standard, widely-used and easily available tools or try to build a pure LLVM pipeline.</p> <h3 id="gcc">GCC</h3> <p>The required packages on a Ubuntu-based distro are <strong>binutils</strong> for standard GNU linker <strong>ld</strong>, <strong>libc-dev</strong> for standard C library and <strong>libgcc-${VERSION}-dev</strong> to provide runtime library and stack unwinder. Atomics require additional libraries<sup id="fnref:2:1"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>.</p> <h3 id="clang">Clang</h3> <p>On Debian-based distributions, I recommend to use <strong>libc-6-dev</strong> since it’s perfectly compatible with clang programs. The necessary runtime functions are already provided with <strong>compiler-rt</strong>. Common alternatives to <strong>GNU ld</strong> linker are <strong>GNU gold</strong> or <strong>LLVM lld</strong>.</p> <p>Again, the <strong>Docker image</strong> with the complete build is available as <a href="https://hub.docker.com/repository/docker/mcopik/clang-dfsan/tags"><code class="language-plaintext highlighter-rouge">mcopik/clang-dfsan:dfsan-${VERSION}</code></a>. It comes with two wrappers, <code class="language-plaintext highlighter-rouge">clang-dfsan</code> and <code class="language-plaintext highlighter-rouge">clang++-dfsan</code>, that include flags necessary to enable dataflow sanitization and link against dedicated build of <code class="language-plaintext highlighter-rouge">libc++</code>.</p> <h2 id="openmp">OpenMP</h2> <p>LLVM has a separate implementation of <a href="https://openmp.llvm.org/">OpenMP</a>, a widely-used library for multithreading and shared-memory parallelism. The build steps are similar to other project, with minor differences. First, an additional CMake parameter <code class="language-plaintext highlighter-rouge">LIBOMP_ENABLE_SHARED=Off</code> is used to enforce building a static library. Second, there are several functions that cannot be instrumented because their implementations are provided only in assembly. On Linux, these functions are provided in <code class="language-plaintext highlighter-rouge">runtime/src/z_Linux_asm.S</code> and dfsan blacklist has to be updated:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fun:__kmp_x86_cpuid=uninstrumented
fun:__kmp_x86_cpuid=discard
fun:__kmp_store_x87_fpu_control_word=uninstrumented
fun:__kmp_store_x87_fpu_control_word=discard
fun:__kmp_load_x87_fpu_control_word=uninstrumented
fun:__kmp_load_x87_fpu_control_word=discard
fun:__kmp_clear_x87_fpu_status_word=uninstrumented
fun:__kmp_clear_x87_fpu_status_word=functional
fun:__kmp_hardware_timestamp=uninstrumented
fun:__kmp_hardware_timestamp=discard

fun:__kmp_fork_call=uninstrumented
fun:__kmp_fork_call=functional
fun:__kmp_serialized_parallel=uninstrumented
fun:__kmp_serialized_parallel=functional
fun:__kmp_invoke_microtask=uninstrumented
fun:__kmp_invoke_microtask=functional
</code></pre></div></div> <p>The library can be build similarly to the previous step:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cmake <span class="nt">-G</span> <span class="s2">"Ninja"</span><span class="se">\</span>
  <span class="nt">-DCMAKE_BUILD_TYPE</span><span class="o">=</span>MinSizeRel<span class="se">\</span>
  <span class="nt">-DCMAKE_INSTALL_PREFIX</span><span class="o">=</span>/opt/llvm<span class="se">\</span>
  <span class="nt">-DCMAKE_C_COMPILER</span><span class="o">=</span>clang<span class="se">\</span>
  <span class="nt">-DCMAKE_CXX_COMPILER</span><span class="o">=</span>clang++<span class="se">\</span>
  <span class="nt">-DCMAKE_C_FLAGS</span><span class="o">=</span><span class="s2">"-fsanitize=dataflow -fsanitize-blacklist=/dfsan_abilist.txt"</span><span class="se">\</span>
  <span class="nt">-DCMAKE_CXX_FLAGS</span><span class="o">=</span><span class="s2">"-fsanitize=dataflow -fsanitize-blacklist=/dfsan_abilist.txt"</span><span class="se">\</span>
  <span class="nt">-DLIBCXX_ENABLE_SHARED</span><span class="o">=</span>OFF<span class="se">\</span>
  <span class="nt">-DLIBCXX_CXX_ABI</span><span class="o">=</span>libcxxabi<span class="se">\</span>
  <span class="nt">-DLIBCXX_ENABLE_STATIC_ABI_LIBRARY</span><span class="o">=</span>ON<span class="se">\</span>
  <span class="nt">-DLIBCXX_CXX_ABI_INCLUDE_PATHS</span><span class="o">=</span>../libcxxabi/include/<span class="se">\</span>
  <span class="nt">-DLIBCXX_CXX_ABI_LIBRARY_PATH</span><span class="o">=</span>../build_libcxxabi/lib/<span class="se">\</span>
  ../libcxx
</code></pre></div></div> <p>Finally, we can try to compile a program parallelized with OpenMP by adding the flags <code class="language-plaintext highlighter-rouge">-I/path/to/openmp/include -fopenmp /path/to/openmp/lib/libiomp5.a</code>.</p> <div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;vector&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;numeric&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;cassert&gt;</span><span class="cp">
</span>
<span class="cp">#include</span> <span class="cpf">&lt;omp.h&gt;</span><span class="cp">
</span>
<span class="cp">#include</span> <span class="cpf">&lt;sanitizer/dfsan_interface.h&gt;</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
<span class="p">{</span>

  <span class="kt">int</span> <span class="n">m</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
  <span class="n">dfsan_label</span> <span class="n">m_label</span> <span class="o">=</span> <span class="n">dfsan_create_label</span><span class="p">(</span><span class="s">"m"</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
  <span class="n">dfsan_set_label</span><span class="p">(</span><span class="n">m_label</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">m</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">m</span><span class="p">));</span>
  <span class="kt">int</span> <span class="n">n</span> <span class="o">=</span> <span class="mi">100</span><span class="p">;</span>

  <span class="cp">#pragma omp parallel
</span>  <span class="p">{</span>
    <span class="kt">int</span> <span class="o">*</span> <span class="n">sum</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="p">[</span><span class="n">omp_get_num_threads</span><span class="p">()];</span>
    <span class="cp">#pragma omp for
</span>    <span class="k">for</span><span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">n</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span>
      <span class="n">sum</span><span class="p">[</span><span class="n">omp_get_thread_num</span><span class="p">()]</span> <span class="o">+=</span> <span class="n">m</span><span class="p">;</span>
    <span class="n">dfsan_label</span> <span class="n">test_label</span> <span class="o">=</span> <span class="n">dfsan_read_label</span><span class="p">(</span><span class="o">&amp;</span><span class="n">sum</span><span class="p">[</span><span class="n">omp_get_thread_num</span><span class="p">()],</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">int</span><span class="p">));</span>
    <span class="n">assert</span><span class="p">(</span><span class="n">dfsan_has_label</span><span class="p">(</span><span class="n">test_label</span><span class="p">,</span> <span class="n">m_label</span><span class="p">));</span>
    <span class="k">delete</span><span class="p">[]</span> <span class="n">sum</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div> <h2 id="mpi">MPI</h2> <p>The MPI can be compiled with dfsan support, an example of build for OpenMPI:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">CC</span><span class="o">=</span><span class="k">${</span><span class="nv">CLANG</span><span class="k">}</span> <span class="nv">CFLAGS</span><span class="o">=</span><span class="s2">"-fsanitize=dataflow"</span><span class="se">\</span>
  <span class="o">&amp;&amp;</span> <span class="k">${</span><span class="nv">OPENMPI_DIR</span><span class="k">}</span>/configure<span class="se">\</span>
  <span class="o">&amp;&amp;</span> <span class="nt">--enable-static</span><span class="o">=</span><span class="nb">yes</span><span class="se">\</span>
  <span class="o">&amp;&amp;</span> <span class="nt">--enable-shared</span><span class="o">=</span><span class="nb">false</span>
</code></pre></div></div> <p>Compiling the library requires additional blacklist update because small assembly functions are used during the configuration to test the compiler:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fun:gsym_test_func=uninstrumented
</code></pre></div></div> <p>Apart from that change, the compilation should work seamlessly. C++ compiler should be unnecessary because C++ bindings are deprecated and not built by default</p> <p>An entirely different question is if instrumenting MPI is even necessary? MPI operations tend to overwrite memory content with data received from network and it requires an additional library to handle label propagation through MPI messages. Furthermore, using a dedicated build prevents from running the program on a supercomputer. Thus, a better option might be to simply put MPI functions in the blacklist.</p> <h2 id="references">References</h2> <div class="footnotes" role="doc-endnotes"> <ol> <li id="fn:1"> <p><a href="https://en.wikipedia.org/wiki/Taint_checking">Taint checking on Wikipedia</a> <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> <li id="fn:2"> <p><a href="http://bitblaze.cs.berkeley.edu/papers/dta++-ndss11.pdf">Kang et al., “DTA++: Dynamic Taint Analysis withTargeted Control-Flow Propagation”</a> <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:2:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p> </li> <li id="fn:3"> <p><a href="https://clang.llvm.org/docs/Toolchain.html">Clang: Assembling a Complete Toolchain</a> <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> <li id="fn:4"> <p><a href="http://lists.llvm.org/pipermail/openmp-dev/2016-January/001051.html">January 2016, [Openmp-dev] Building a static LLVM OpenMP library?</a> <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> </ol> </div>]]></content><author><name></name></author><category term="c++"/><category term="c++"/><category term="llvm"/><category term="taint"/><summary type="html"><![CDATA[Clang comes with a set of tools known as sanitizers that provide a runtime verification of common problems such as memory issues and undefined behavior. An interesting and a not well-known one is DfSan, a dataflow sanitizer. The name does not really reveal the true purpose: the library brings a compiler pass and runtime to implement taint analysis1. The main purpose is to taint specific memory regions and automatically propagate taint labels to other locations in memory that are affected by originally tainted regions. Taint labels are stored separately, in a so-called shadow memory, and compiler pass instruments codes with taint propagation. Thus, it is possible for every variable in a program to detect which values have affected it. The analysis is not only fully inter-procedural but it is completely memory agnostic, which is a common issue for static compiler analyses. The sanitizer implements data-flow tainting, the most common way of propagating labels: Taint checking on Wikipedia &#8617;]]></summary></entry></feed>