<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Efrain Garay</title>
    <description>The latest articles on DEV Community by Efrain Garay (@efraingaray).</description>
    <link>https://dev.to/efraingaray</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4086497%2Ff46e5349-16a6-4bd9-b4b4-7361b88f8578.jpg</url>
      <title>DEV Community: Efrain Garay</title>
      <link>https://dev.to/efraingaray</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/efraingaray"/>
    <language>en</language>
    <item>
      <title>Don't put getaddrinfo on your proxy's hot path</title>
      <dc:creator>Efrain Garay</dc:creator>
      <pubDate>Thu, 10 Sep 2026 21:11:48 +0000</pubDate>
      <link>https://dev.to/efraingaray/dont-put-getaddrinfo-on-your-proxys-hot-path-i31</link>
      <guid>https://dev.to/efraingaray/dont-put-getaddrinfo-on-your-proxys-hot-path-i31</guid>
      <description>&lt;p&gt;I set out to benchmark Pingora 0.9.0 — the Rust proxy library Cloudflare runs at its edge — against nginx, in the environment I actually care about: a small pod with a CPU limit, two vCPUs, everything in containers so no number leans on the host.&lt;/p&gt;

&lt;p&gt;I wrote the simplest possible reverse proxy with Pingora, hit it, and got &lt;strong&gt;21k requests/sec against nginx's 126k in the same setup&lt;/strong&gt;. Six times slower.&lt;/p&gt;

&lt;p&gt;That number was a lie, and the fault was mine.&lt;/p&gt;

&lt;p&gt;Six times is too much — Cloudflare would not replace nginx with something 6x slower. So I isolated it. Not CPU throttling (&lt;code&gt;nr_throttled&lt;/code&gt; was zero). Not thread oversubscription (exactly two workers). Not connection reuse (both held ~130 upstream connections). The clue was latency: 4.78 ms in the container, 0.45 ms on the host with the backend at &lt;code&gt;127.0.0.1&lt;/code&gt;. The only thing that changed was how I named the backend.&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;upstream_peer&lt;/code&gt; built the destination on &lt;strong&gt;every request&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// (&amp;amp;str, u16) → to_socket_addrs() → getaddrinfo, blocking, on the tokio worker&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;peer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;HttpPeer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s"&gt;"backend"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;String&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;HttpPeer::new&lt;/code&gt; over a &lt;code&gt;(&amp;amp;str, u16)&lt;/code&gt; calls &lt;code&gt;getaddrinfo&lt;/code&gt; synchronously, inside the tokio worker thread. In a container, every request fired a DNS query to Docker's resolver and blocked the thread. On the host, an IP literal is just a parse — no syscall — so it barely showed.&lt;/p&gt;

&lt;p&gt;The fix is one line: resolve once at startup, keep the &lt;code&gt;SocketAddr&lt;/code&gt;, pass that on the hot path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;addr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"backend"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.to_socket_addrs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// ...on every request, no lookup:&lt;/span&gt;
&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;HttpPeer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.addr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;String&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;())))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pingora went from 21k to &lt;strong&gt;89k req/s&lt;/strong&gt; in the same pod. The 6x was a &lt;code&gt;getaddrinfo&lt;/code&gt; per request, not the framework.&lt;/p&gt;

&lt;p&gt;The lesson isn't about Pingora. A microbenchmark punishes any clumsiness of whoever writes it, and a blocking name resolution hidden on the hot path disguises itself perfectly as "the framework is slow."&lt;/p&gt;

&lt;p&gt;With the proxy written properly, the &lt;strong&gt;real gap in a 2 vCPU pod is 1.49x&lt;/strong&gt; — and &lt;code&gt;perf&lt;/code&gt; pins it down to the cycle: Pingora runs 1.7x more instructions per request. I also found Pingora's thread default is 1 (a whole idle core if you don't set it), and that in a CPU-limited pod, &lt;em&gt;more&lt;/em&gt; threads than cores wrecks the p99 through CFS throttling.&lt;/p&gt;

&lt;p&gt;I documented the whole thing — the container setup, the full concurrency matrix with repetitions, the &lt;code&gt;perf&lt;/code&gt;/&lt;code&gt;strace&lt;/code&gt; profile, the flamegraph, and the thread-throttling numbers — with live diagrams and a reel, here:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://efraingaray.com/en/blog/pingora-090-getaddrinfo-hot-path/" rel="noopener noreferrer"&gt;The full write-up on efraingaray.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you benchmark a Rust proxy in a container: resolve upstreams by IP or once, and set threads equal to your CPU quota. Those two lines matter more than the framework you pick.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>programming</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>pg_anon caught 1 of my 8 PII columns. My schema isn't in English.</title>
      <dc:creator>Efrain Garay</dc:creator>
      <pubDate>Sun, 06 Sep 2026 03:20:27 +0000</pubDate>
      <link>https://dev.to/efraingaray/pganon-caught-1-of-my-8-pii-columns-my-schema-isnt-in-english-2e5d</link>
      <guid>https://dev.to/efraingaray/pganon-caught-1-of-my-8-pii-columns-my-schema-isnt-in-english-2e5d</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/TantorLabs/pg_anon" rel="noopener noreferrer"&gt;pg_anon&lt;/a&gt; found &lt;strong&gt;1&lt;/strong&gt; of the 8 personal-data columns in my PostgreSQL database. The one it caught was &lt;code&gt;email&lt;/code&gt;, and only because "email" is spelled the same in Spanish and English.&lt;/p&gt;

&lt;p&gt;The other seven — &lt;code&gt;nombre&lt;/code&gt;, &lt;code&gt;apellido&lt;/code&gt;, &lt;code&gt;telefono&lt;/code&gt;, &lt;code&gt;direccion&lt;/code&gt;, &lt;code&gt;fecha_nac&lt;/code&gt;, &lt;code&gt;tarjeta_ult4&lt;/code&gt; and &lt;code&gt;rut&lt;/code&gt; — walked straight through, unmasked.&lt;/p&gt;

&lt;p&gt;pg_anon is the open TantorLabs tool that masks personal data in PostgreSQL: it scans the database, flags the sensitive columns, and dumps a masked copy. Like &lt;code&gt;pg_dump&lt;/code&gt;, but covering the sensitive parts on the way out. Exactly what you want before handing a colleague a copy of production. So I fed it a Chilean database and watched it miss almost everything — no error, no warning. It finished successfully and handed me a dump with names and national IDs still in cleartext.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the scan actually does
&lt;/h2&gt;

&lt;p&gt;Two filters, in order. First it reads each column's &lt;strong&gt;name&lt;/strong&gt; against a set of regexes (&lt;code&gt;^email$&lt;/code&gt;, &lt;code&gt;^phone$&lt;/code&gt;, &lt;code&gt;^ssn$&lt;/code&gt;…). To the columns left over, it opens the &lt;strong&gt;data&lt;/strong&gt; and tries patterns on the value (an email's &lt;code&gt;@&lt;/code&gt;, a card's 16 digits). Whatever no filter catches passes through.&lt;/p&gt;

&lt;p&gt;The rules in the demo meta-dict it ships with are written for English schemas. Mine aren't.&lt;/p&gt;

&lt;h2&gt;
  
  
  1 of 8
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Holds&lt;/th&gt;
&lt;th&gt;Stock rules&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;email&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nombre&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;first name&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apellido&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;surname&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;telefono&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;phone&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;direccion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;address&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fecha_nac&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;birth date&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tarjeta_ult4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;card digits&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rut&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;national ID&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;email&lt;/code&gt; got caught by name (it's an English word) and confirmed by its &lt;code&gt;@&lt;/code&gt;. Everything else has a Spanish name no stock rule looks for.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;rut&lt;/code&gt; is the clearest miss. A RUT looks like &lt;code&gt;7917183-2&lt;/code&gt;: seven or eight digits, a dash, and a mod-11 check digit that &lt;strong&gt;can be the letter K&lt;/strong&gt;. The only national-ID rule pg_anon ships with is &lt;code&gt;ssn&lt;/code&gt;. It has no idea what a RUT is — and it won't know a &lt;code&gt;cpf&lt;/code&gt; (Brazil), &lt;code&gt;dni&lt;/code&gt; (Spain, Argentina), &lt;code&gt;curp&lt;/code&gt; (Mexico), &lt;code&gt;nif&lt;/code&gt; (Portugal) or &lt;code&gt;aadhaar&lt;/code&gt; (India) either. If your schema isn't American, the defaults miss your most sensitive column.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: a few lines of Spanish
&lt;/h2&gt;

&lt;p&gt;You teach it. Column names in your language, plus a content regex for the ID so it's caught even in a column &lt;em&gt;not&lt;/em&gt; named &lt;code&gt;rut&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rules&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^nombre$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^apellido$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^email$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^telefono$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^rut$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^direccion$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^fecha_nac$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^tarjeta_ult4$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data_regex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rules&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[\w.-]+@[\w-]+\.\w+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# email
&lt;/span&gt;      &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^(\+?56)?9\d{8}$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# Chilean mobile
&lt;/span&gt;      &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^\d{5,8}-[\dkK]$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# RUT with check digit
&lt;/span&gt;    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that, recall went from &lt;strong&gt;1 of 8 to 8 of 8&lt;/strong&gt;, no false positives — it still left the non-personal columns alone. The point isn't "it went up when I wrote the rules" (obviously). It's how cheap the fix was, and that a masking tool ships blind to any schema that isn't English.&lt;/p&gt;

&lt;p&gt;I left the full Spanish meta-dict (with the RUT regex) in a public repo so nobody starts from scratch: &lt;strong&gt;&lt;a href="https://github.com/EfrainGaray/pg-anon-rules-es" rel="noopener noreferrer"&gt;pg-anon-rules-es&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;This is the short version. The full write-up covers what didn't fit: whether foreign keys survive the restore, the trap where a blanket SHA-256 keeps uniqueness but produces "emails" with no &lt;code&gt;@&lt;/code&gt;, and how much slower it runs than a plain &lt;code&gt;pg_dump&lt;/code&gt;. Numbers, diagrams and a 47-second breakdown: &lt;strong&gt;&lt;a href="https://efraingaray.com/en/blog/pg-anon-datos-personales" rel="noopener noreferrer"&gt;pg_anon found my database's emails, but not the RUT&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does the national ID look like where you are — and would a stock masking tool catch it?&lt;/strong&gt; Drop the format (or the regex) in the comments and I'll add it to the repo.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>security</category>
      <category>programming</category>
    </item>
    <item>
      <title>I translated DOOM from C to Rust in 23 seconds, then used AI for what it is good at</title>
      <dc:creator>Efrain Garay</dc:creator>
      <pubDate>Fri, 21 Aug 2026 21:58:13 +0000</pubDate>
      <link>https://dev.to/efraingaray/i-translated-doom-from-c-to-rust-in-23-seconds-then-used-ai-for-what-it-is-good-at-1j3a</link>
      <guid>https://dev.to/efraingaray/i-translated-doom-from-c-to-rust-in-23-seconds-then-used-ai-for-what-it-is-good-at-1j3a</guid>
      <description>&lt;p&gt;I needed to move a C program to Rust. My first instinct was the easy one: hand it to a model, chunk by chunk, and review the output.&lt;/p&gt;

&lt;p&gt;Then I checked whether a tool already existed. &lt;code&gt;c2rust&lt;/code&gt;, from Immunant, does exactly this. On the 73,095 lines of doomgeneric it took &lt;strong&gt;23.2 seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The interesting part is not the speed. It is that the model still ended up doing real work, just nowhere near the translation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost was not the argument, and pretending otherwise would be selling smoke
&lt;/h2&gt;

&lt;p&gt;I measured the token cost of translating DOOM with a model using the &lt;code&gt;o200k_base&lt;/code&gt; tokenizer: 550,595 input tokens and 1,896,319 output. That is a few dollars. It is not a reason.&lt;/p&gt;

&lt;p&gt;What actually decides is three other things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reproducible.&lt;/strong&gt; Same input, same output file, byte for byte, every run. You can &lt;code&gt;diff&lt;/code&gt; it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Systematic errors.&lt;/strong&gt; The translation produced four bugs. Two were the same bug in 23 places. One class gets fixed once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All of them failed loudly.&lt;/strong&gt; Every one crashed at compile time or on the first frame. A scattered, silent error in one of 143,911 lines costs more to find than the whole translation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How you prove a translation did not change the program
&lt;/h2&gt;

&lt;p&gt;DOOM ships recorded demos. Every gameplay decision is a fixed-point calculation, so if one of them differs the playback desynchronizes and the tic count stops matching. That is the first gate, and it is free.&lt;/p&gt;

&lt;p&gt;The second gate: I hashed every frame the engine hands to the display, in both builds. &lt;strong&gt;11,113 frames, one differed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I nearly published that one frame as &lt;code&gt;c2rust&lt;/code&gt;'s single error. The control run is what saved me: I ran the original C binary against &lt;strong&gt;itself&lt;/strong&gt;. It differs from itself too, on the same pixel. Original DOOM reads one pixel of uninitialized memory, and the translation reproduced that faithfully.&lt;/p&gt;

&lt;p&gt;If you only ever compare against one run, you will report your own reference's noise as the other side's bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The translated Rust is not safe Rust
&lt;/h2&gt;

&lt;p&gt;Measuring &lt;strong&gt;lines inside &lt;code&gt;unsafe&lt;/code&gt; blocks&lt;/strong&gt;, the translated zlib puts 47% of the crate in &lt;code&gt;unsafe&lt;/code&gt;, against 31% for &lt;code&gt;zlib-rs&lt;/code&gt;, a rewrite done by people.&lt;/p&gt;

&lt;p&gt;Counting occurrences of the word &lt;code&gt;unsafe&lt;/code&gt; gives you the opposite answer, and it is a misleading metric: the translator wraps whole functions in a single block, so it scores &lt;em&gt;better&lt;/em&gt; on a count while being worse in reality.&lt;/p&gt;

&lt;p&gt;These are not comparable artifacts anyway. &lt;code&gt;zlib-rs&lt;/code&gt; is a rewrite, with redesigned structures and the type system doing work. An automatic translation cannot do that. Its goal is to not change the program.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the model actually earned its place
&lt;/h2&gt;

&lt;p&gt;Not translating. Generating detail that &lt;strong&gt;modulates on top of what the engine already computes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every texture goes through an upscaler, then gets stored as a one-channel map whose mean is exactly 128 per texel. The renderer keeps deciding color and lighting; the map only contributes variation &lt;em&gt;within&lt;/em&gt; the texel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;color_final = color_doom × detail / 128
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the mean is anchored at 128, the enhancement provably cannot shift the image. Averaged over any texel, it multiplies by 1.&lt;/p&gt;

&lt;p&gt;And that same map is a height map, so its gradient gives per-pixel relief for free, with no extra bytes, baked at pack time.&lt;/p&gt;

&lt;p&gt;That constraint, that the engine keeps deciding, is what made the result add instead of ruin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Both versions are playable in your browser
&lt;/h2&gt;

&lt;p&gt;The 1993 C build and the Rust translation are compiled to WebAssembly and running side by side, plus the enhanced renderer at 640×400 with per-pixel lighting.&lt;/p&gt;

&lt;p&gt;The 60-second version of all of this, with the game running behind it:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/2mTM5vuS114"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;p&gt;The full write-up has the four translation bugs, the three portability ones, the &lt;code&gt;memset&lt;/code&gt; that silently cleared half the screen after widening a struct field, and how it went from 7215 to 3009 realtics without rewriting the game:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://efraingaray.com/en/blog/c-a-rust-doom/" rel="noopener noreferrer"&gt;I translated DOOM from C to Rust without writing a line, then used AI for what it is actually good at&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>programming</category>
      <category>ai</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Go 1.27's SIMD ties with NumPy until the data fits in cache</title>
      <dc:creator>Efrain Garay</dc:creator>
      <pubDate>Thu, 20 Aug 2026 17:02:12 +0000</pubDate>
      <link>https://dev.to/efraingaray/go-127s-simd-ties-with-numpy-until-the-data-fits-in-cache-2bm8</link>
      <guid>https://dev.to/efraingaray/go-127s-simd-ties-with-numpy-until-the-data-fits-in-cache-2bm8</guid>
      <description>&lt;p&gt;Go 1.27 shipped a &lt;code&gt;simd&lt;/code&gt; package in the standard library. It is experimental and sits behind a flag, and every write-up I found described the API. None of them said when it actually helps.&lt;/p&gt;

&lt;p&gt;So I measured it against NumPy on the same task: a speaker-search index, 346 thousand vectors of 192 dimensions, 66 million multiplications per query.&lt;/p&gt;

&lt;p&gt;The answer turned out not to be about Go or NumPy at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same code, two verdicts
&lt;/h2&gt;

&lt;p&gt;With a &lt;strong&gt;253 MB&lt;/strong&gt; corpus, single-threaded:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;median&lt;/th&gt;
&lt;th&gt;spread&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Go SIMD&lt;/td&gt;
&lt;td&gt;9.54 ms&lt;/td&gt;
&lt;td&gt;14%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NumPy (OpenBLAS)&lt;/td&gt;
&lt;td&gt;9.76 ms&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is a tie, not a win. The 2% gap fits entirely inside the noise.&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;31 MB&lt;/strong&gt;, which fits in the L3 cache:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;median&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NumPy (OpenBLAS)&lt;/td&gt;
&lt;td&gt;0.393 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go SIMD&lt;/td&gt;
&lt;td&gt;0.943 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;NumPy is &lt;strong&gt;2.4× faster&lt;/strong&gt;. Same code, same machine, same afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the reversal
&lt;/h2&gt;

&lt;p&gt;The number that explains it is not in either table. Parallelising across eight physical cores gave &lt;strong&gt;2.9× in Go and 3.0× in NumPy&lt;/strong&gt;. Pure computation would give close to 8. Both stopping at 3 is the tell: the ceiling is not the code.&lt;/p&gt;

&lt;p&gt;At 253 MB nothing fits in any cache, so both programs spend most of their time waiting on memory. When two implementations hit that wall, code quality stops mattering. Twenty lines of a brand-new portable package match assembly that has been hand-tuned per microarchitecture for years.&lt;/p&gt;

&lt;p&gt;At 31 MB the data sits in L3, the bottleneck goes back to arithmetic, and OpenBLAS's years of tuning show up immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What decides is not the language. It is whether the data fits in cache.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The 55-second version of all of this, narrated:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/LT_wboeXeqs"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The part worth stealing
&lt;/h2&gt;

&lt;p&gt;I nearly published the opposite conclusion, twice.&lt;/p&gt;

&lt;p&gt;The first run gave NumPy double Go's speed. OpenBLAS spreads work across every core without announcing it, while my Go program ran on one thread. &lt;code&gt;OMP_NUM_THREADS=1&lt;/code&gt; and &lt;code&gt;OPENBLAS_NUM_THREADS=1&lt;/code&gt; made NumPy three times slower and flipped the result.&lt;/p&gt;

&lt;p&gt;The second time I measured a machine with thirteen foreign processes at 100% CPU and did not notice. The same numbers came out ten times worse on the check run.&lt;/p&gt;

&lt;p&gt;So the harness pins cores with &lt;code&gt;taskset&lt;/code&gt;, interleaves the implementations round by round instead of measuring all of A then all of B, aborts if load exceeds 2.0, and reports median and spread rather than the best time. The best time measures the best case and hides exactly the instability that matters.&lt;/p&gt;

&lt;p&gt;Its first version printed &lt;code&gt;0.000 ms&lt;/code&gt; for NumPy. Not a record: &lt;code&gt;perf_counter&lt;/code&gt; returns seconds and I was printing milliseconds. A suspicious zero is a units bug before it is a marvel.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not prove
&lt;/h2&gt;

&lt;p&gt;Two home machines, one embedding model, two corpus sizes. My Go code walks vector by vector while OpenBLAS solves the whole matrix-vector product with blocking and unrolling. That is a naive implementation against an expert one, which is exactly what you get picking the package up fresh.&lt;/p&gt;

&lt;p&gt;The exact numbers belong to this hardware. The shape of the curve repeated on both machines, with synthetic and real data, and that is the only part I would call general.&lt;/p&gt;




&lt;p&gt;The full write-up has the synthetic benchmark across three cache levels, the control that separates data size from data nature, a second machine that chose 4 lanes when it could have used 8, and the raw numbers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://efraingaray.com/en/blog/go-simd-vs-numpy/" rel="noopener noreferrer"&gt;Go 1.27 brings portable SIMD: it ties with NumPy out of cache and loses inside it&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>performance</category>
      <category>programming</category>
      <category>python</category>
    </item>
  </channel>
</rss>
