<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Posts on Push to Mastery</title><link>https://pessolato.github.io/posts/</link><description>Recent content in Posts on Push to Mastery</description><generator>Hugo -- gohugo.io</generator><language>en</language><copyright>&lt;a href='https://creativecommons.org/licenses/by-nc/4.0/' target='_blank' rel='noopener'>CC BY-NC 4.0&lt;/a></copyright><lastBuildDate>Sun, 21 Sep 2025 12:58:36 +0200</lastBuildDate><atom:link href="https://pessolato.github.io/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>Go HTTP: Impact of NOT Reading Response Bodies to EOF</title><link>https://pessolato.github.io/posts/go-http-drain-close-microbench/</link><pubDate>Sun, 21 Sep 2025 12:58:36 +0200</pubDate><guid>https://pessolato.github.io/posts/go-http-drain-close-microbench/</guid><description>&lt;p>Recently, a colleague and I were discussing whether draining the response body (my shorthand for “reading to EOF before closing”) was actually necessary when the response contains an error code. In that situation, the client already reads the full body on successful requests, so my colleague argued that skipping the drain on error responses would have negligible performance impact.&lt;/p>
&lt;p>That reasoning made sense at first glance, but it left me curious. I knew the theory, but I wanted to quantify the actual performance impact of not draining response bodies.&lt;/p></description><content type="html"><![CDATA[<p>Recently, a colleague and I were discussing whether draining the response body (my shorthand for “reading to EOF before closing”) was actually necessary when the response contains an error code. In that situation, the client already reads the full body on successful requests, so my colleague argued that skipping the drain on error responses would have negligible performance impact.</p>
<p>That reasoning made sense at first glance, but it left me curious. I knew the theory, but I wanted to quantify the actual performance impact of not draining response bodies.</p>
<p>This post documents that investigation.</p>
<blockquote>
<p>Source code of this experiment can be found in <a href="https://github.com/pessolato/httpmicrobench">this GitHub repository</a>.</p></blockquote>
<h2 id="why-this-matters">Why This Matters</h2>
<p>HTTP keep-alive and connection reuse are critical for performance in modern applications. If clients fail to properly consume responses, they can inadvertently disable connection pooling, forcing expensive new TCP (and potentially TLS) handshakes on each request.</p>
<p>This problem is subtle: your code may work fine in tests, but at scale, the performance degradation can be dramatic.</p>
<h2 id="how-gos-http-transport-works">How Go’s HTTP Transport Works</h2>
<p>When you issue a request with <code>http.Client.Do(req)</code>:</p>
<ol>
<li>The request is dispatched through the configured <code>Transport</code> (by default, <code>http.Transport</code>).</li>
<li>The transport either opens or reuses a TCP connection to the server.</li>
<li>The server replies with headers followed by a body (either length-delimited or chunked).</li>
<li>Go exposes the body as <code>resp.Body</code>, which is more than a simple <code>io.ReadCloser</code>. It manages the underlying connection and determines whether it can be reused.</li>
</ol>
<h3 id="case-1-closing-without-reading-to-eof">Case 1: Closing Without Reading to EOF</h3>
<p>If you call <code>resp.Body.Close()</code> before consuming all bytes:</p>
<ul>
<li>
<p><strong>Unread bytes remain in the buffer.</strong>
Suppose the server sends <code>Content-Length: 1000</code>, but you only read 100 bytes. The remaining 900 bytes still sit in the TCP buffer.</p>
</li>
<li>
<p><strong>Connection reuse becomes unsafe.</strong>
The <code>Transport</code> cannot guarantee the connection is aligned with the next request boundary.</p>
</li>
<li>
<p><strong>The connection is discarded.</strong>
Instead of returning the connection to the idle pool, Go closes it.</p>
</li>
<li>
<p><strong>Practical effect:</strong> The next request must open a fresh TCP (and TLS) connection, which increases latency and CPU load.</p>
</li>
</ul>
<h3 id="case-2-reading-to-eof-and-closing">Case 2: Reading to EOF and Closing</h3>
<p>If you fully consume the response body:</p>
<ul>
<li>The <code>Transport</code> can confirm the stream is aligned.</li>
<li>Closing returns the connection to the idle pool.</li>
<li>Subsequent requests reuse the same connection, avoiding extra handshakes.</li>
</ul>
<h2 id="designing-the-experiment">Designing the Experiment</h2>
<p>To measure the performance impact, I built a controlled test environment using only Go’s standard library and Docker for orchestration. The setup consisted of a simple HTTP client and server, plus infrastructure to collect and analyze metrics.</p>
<h3 id="server">Server</h3>
<p>The server responds to any request with a payload of configurable size. A path parameter controls the number of bytes to send back, which the server generates as random data. This allowed me to simulate different response sizes without external dependencies.</p>
<h3 id="client">Client</h3>
<p>The client was more involved. Key requirements:</p>
<ul>
<li><strong>Protocol selection:</strong> Ability to enable only HTTP/1.1 or HTTP/2.</li>
<li><strong>Tracing:</strong> Integration with <code>httptrace.ClientTrace</code> to record connection and request lifecycle events.</li>
<li><strong>Configurable draining:</strong> Optionally read responses to EOF before closing.</li>
</ul>
<p>Each client issues a “template” request <code>n</code> times in sequence, logging response times and tracing events. Configuration is done via environment variables.</p>
<h3 id="orchestration-with-docker">Orchestration with Docker</h3>
<p>The most complex part was orchestration. I used the Docker SDK to build images, manage networks, and control containers. While arguably over-engineered, the setup gave me fine-grained control over test runs.</p>
<p>The workflow:</p>
<ol>
<li>
<p><strong>Image and network setup</strong></p>
<ul>
<li>Compile client and server into separate binaries.</li>
<li>Build Docker images if missing.</li>
<li>Create a dedicated network for isolation.</li>
</ul>
</li>
<li>
<p><strong>Container creation</strong></p>
<ul>
<li>
<p>Six containers in total:</p>
<ul>
<li>Four clients: HTTP/1.1 with/without draining, HTTP/2 with/without draining.</li>
<li>Two servers: one for draining clients, one for non-draining.</li>
</ul>
</li>
</ul>
</li>
<li>
<p><strong>Data collection</strong></p>
<ul>
<li>Logs and metrics streamed via the Docker API.</li>
<li>Data stored in JSONL format for easy parsing.</li>
</ul>
</li>
<li>
<p><strong>Cleanup</strong></p>
<ul>
<li>Containers are removed after each run.</li>
</ul>
</li>
</ol>
<h2 id="running-the-benchmark">Running the Benchmark</h2>
<p>Each client issued 10,000 sequential requests with the following command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>NUMBER_OF_REQUESTS<span style="color:#f92672">=</span><span style="color:#ae81ff">10000</span> go run ./cmd/bench/
</span></span></code></pre></div><p>To summarize results, I ran:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>BENCH_RESULTS_DIRECTORY<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;benchresults/20250920155945&#34;</span> go run ./cmd/stats/
</span></span></code></pre></div><p>The stats tool computed min, max, mean, and median for response times and CPU usage. While I focused on latency and CPU, the dataset also allows analyzing connection reuse, memory usage, and error rates.</p>
<h2 id="results">Results</h2>
<p>The following is the output of the stats tool:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Summarizing result logs from file: benchresults/20250920155945/client-http-1-drain-0-logs.jsonl
</span></span><span style="display:flex;"><span>Request Time:
</span></span><span style="display:flex;"><span>- Min: 462.968µs
</span></span><span style="display:flex;"><span>- Max: 4.002694458s
</span></span><span style="display:flex;"><span>- Mean: 1.279138ms
</span></span><span style="display:flex;"><span>- Median: 836.046µs
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Summarizing result stats from file: benchresults/20250920155945/client-http-1-drain-0-stats.jsonl
</span></span><span style="display:flex;"><span>CPU Usage:
</span></span><span style="display:flex;"><span>- Min: 0.00%
</span></span><span style="display:flex;"><span>- Max: 114.49%
</span></span><span style="display:flex;"><span>- Mean: 72.64%
</span></span><span style="display:flex;"><span>- Median: 108.39%
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Summarizing result logs from file: benchresults/20250920155945/client-http-1-drain-1-logs.jsonl
</span></span><span style="display:flex;"><span>Request Time:
</span></span><span style="display:flex;"><span>- Min: 104.666µs
</span></span><span style="display:flex;"><span>- Max: 4.01174869s
</span></span><span style="display:flex;"><span>- Mean: 705.606µs
</span></span><span style="display:flex;"><span>- Median: 285.943µs
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Summarizing result stats from file: benchresults/20250920155945/client-http-1-drain-1-stats.jsonl
</span></span><span style="display:flex;"><span>CPU Usage:
</span></span><span style="display:flex;"><span>- Min: 0.00%
</span></span><span style="display:flex;"><span>- Max: 89.40%
</span></span><span style="display:flex;"><span>- Mean: 35.87%
</span></span><span style="display:flex;"><span>- Median: 0.00%
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Summarizing result logs from file: benchresults/20250920155945/client-http-2-drain-0-logs.jsonl
</span></span><span style="display:flex;"><span>Request Time:
</span></span><span style="display:flex;"><span>- Min: 452.761µs
</span></span><span style="display:flex;"><span>- Max: 4.002149171s
</span></span><span style="display:flex;"><span>- Mean: 1.273813ms
</span></span><span style="display:flex;"><span>- Median: 829.245µs
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Summarizing result stats from file: benchresults/20250920155945/client-http-2-drain-0-stats.jsonl
</span></span><span style="display:flex;"><span>CPU Usage:
</span></span><span style="display:flex;"><span>- Min: 0.00%
</span></span><span style="display:flex;"><span>- Max: 113.48%
</span></span><span style="display:flex;"><span>- Mean: 71.06%
</span></span><span style="display:flex;"><span>- Median: 107.84%
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Summarizing result logs from file: benchresults/20250920155945/client-http-2-drain-1-logs.jsonl
</span></span><span style="display:flex;"><span>Request Time:
</span></span><span style="display:flex;"><span>- Min: 105.359µs
</span></span><span style="display:flex;"><span>- Max: 4.001713071s
</span></span><span style="display:flex;"><span>- Mean: 707.408µs
</span></span><span style="display:flex;"><span>- Median: 289.669µs
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Summarizing result stats from file: benchresults/20250920155945/client-http-2-drain-1-stats.jsonl
</span></span><span style="display:flex;"><span>CPU Usage:
</span></span><span style="display:flex;"><span>- Min: 0.00%
</span></span><span style="display:flex;"><span>- Max: 89.51%
</span></span><span style="display:flex;"><span>- Mean: 34.10%
</span></span><span style="display:flex;"><span>- Median: 0.00%
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Summarizing result stats from file: benchresults/20250920155945/server-drain-0-stats.jsonl
</span></span><span style="display:flex;"><span>CPU Usage:
</span></span><span style="display:flex;"><span>- Min: 0.00%
</span></span><span style="display:flex;"><span>- Max: 82.52%
</span></span><span style="display:flex;"><span>- Mean: 51.61%
</span></span><span style="display:flex;"><span>- Median: 79.07%
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Summarizing result stats from file: benchresults/20250920155945/server-drain-1-stats.jsonl
</span></span><span style="display:flex;"><span>CPU Usage:
</span></span><span style="display:flex;"><span>- Min: 0.00%
</span></span><span style="display:flex;"><span>- Max: 120.90%
</span></span><span style="display:flex;"><span>- Mean: 27.23%
</span></span><span style="display:flex;"><span>- Median: 0.00%
</span></span></code></pre></div><p>Highlights from the results:</p>
<ul>
<li><strong>Draining improved mean request times by ~44.5%</strong> (both HTTP/1.1 and HTTP/2).</li>
<li><strong>Median latency improved by ~65%</strong> — the effect on typical requests is even stronger than on the average.</li>
<li><strong>Client CPU usage dropped by 50–52%</strong> when draining.</li>
<li><strong>Server CPU usage dropped by ~47%</strong> when clients drained responses.</li>
<li>Without draining, both client and server CPUs often saturated near 100%; with draining, they frequently idled near 0%.</li>
</ul>
<p>In other words, the cost of not draining is not just a small inefficiency—it can double CPU usage and dramatically increase latency.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Failing to drain HTTP response bodies in Go <strong>significantly harms performance</strong> for both clients and servers.</p>
<ul>
<li><strong>Request latency:</strong> ~44.5% faster on average, ~65% faster at median.</li>
<li><strong>Client CPU:</strong> ~50% lower.</li>
<li><strong>Server CPU:</strong> ~47% lower.</li>
<li><strong>Connection reuse:</strong> Preserved with draining, disabled without it.</li>
</ul>
<h3 id="practical-takeaway">Practical Takeaway</h3>
<p>Always drain the response body before closing, even when handling error responses you don’t care about. If you don’t need the content, discard it by reading to EOF and ignoring the bytes.</p>
<p>This small step ensures connection reuse, reduces CPU load, and dramatically improves performance—both for your client and the servers it communicates with.</p>
]]></content></item><item><title>The Importance of MFA and some misconceptions</title><link>https://pessolato.github.io/posts/mfa-misconceptions/</link><pubDate>Tue, 22 Jul 2025 20:57:48 +0200</pubDate><guid>https://pessolato.github.io/posts/mfa-misconceptions/</guid><description>&lt;p>When discussing the security of information systems, one of the foundational concepts is &lt;strong>authentication&lt;/strong>, which can be defined as the process of verifying the identity of a user, device, or system attempting to access a resource. Authentication answers the question: &lt;em>“Who are you?”&lt;/em>, and it does so by requiring evidence of identity.&lt;/p>
&lt;p>This verification is necessary because, much like in physical security, access to protected spaces or resources must be controlled based on evidence of identity. In the physical world, guards at a secure facility do not simply allow anyone to enter; they check badges, keys, or biometric features, and may even recognize familiar individuals. The possession of a key or ID, knowledge of a code, or inherent physical traits serve as tangible proofs of identity. Similarly, in digital systems, where there is no direct way for the system to “see” or “sense” the claimant, we rely on analogous mechanisms: transmitted credentials and controlled challenges that emulate the same principles of trust established at a physical checkpoint.&lt;/p></description><content type="html"><![CDATA[<p>When discussing the security of information systems, one of the foundational concepts is <strong>authentication</strong>, which can be defined as the process of verifying the identity of a user, device, or system attempting to access a resource. Authentication answers the question: <em>“Who are you?”</em>, and it does so by requiring evidence of identity.</p>
<p>This verification is necessary because, much like in physical security, access to protected spaces or resources must be controlled based on evidence of identity. In the physical world, guards at a secure facility do not simply allow anyone to enter; they check badges, keys, or biometric features, and may even recognize familiar individuals. The possession of a key or ID, knowledge of a code, or inherent physical traits serve as tangible proofs of identity. Similarly, in digital systems, where there is no direct way for the system to “see” or “sense” the claimant, we rely on analogous mechanisms: transmitted credentials and controlled challenges that emulate the same principles of trust established at a physical checkpoint.</p>
<h3 id="the-three-classic-authentication-factors">The Three Classic Authentication Factors</h3>
<p>Historically, the framework for authentication has been formalized in terms of <strong>factors of authentication</strong>, which fall into three mutually distinct categories, often summarized as:</p>
<ul>
<li>
<p><strong>Something you know</strong> — a secret memorized and presented at the time of authentication, such as a password, PIN, or the answer to a security question. This is the oldest and most common factor, dating back to the earliest computer systems, where login prompts asked users to type a password. This factor is inexpensive to implement and does not require specialized hardware, but it suffers from significant weaknesses: secrets can be guessed, stolen, reused, or intercepted, and humans tend to choose weak or repetitive passwords for convenience.</p>
</li>
<li>
<p><strong>Something you have</strong> — a physical or digital token in your possession, such as a smart card, a time-based one-time password (TOTP) generator, a mobile app, or a hardware security key (e.g., FIDO2). The assumption here is that the token is difficult to duplicate and remains under the owner’s control. This factor mitigates some weaknesses of knowledge-based authentication by adding a physical element, but it introduces operational challenges: tokens can be lost, damaged, or stolen, and managing them at scale requires additional logistics and infrastructure.</p>
</li>
<li>
<p><strong>Something you are</strong> — an intrinsic characteristic of your body, or <strong>biometric evidence</strong>, such as a fingerprint, retinal or iris pattern, facial geometry, voice pattern, or even behavioral traits (like typing rhythm or gait). Biometric authentication is compelling because it is hard to forge and convenient for users. However, it also raises concerns about privacy, data permanence (you cannot change your fingerprint if it is compromised), and accuracy (with risks of false positives and false negatives).</p>
</li>
</ul>
<p>These three factors are considered <strong>orthogonal</strong>, meaning that each addresses different kinds of risk and they are stronger when combined. This is the basis of <strong>multi-factor authentication (MFA)</strong>, which requires the claimant to present evidence from at least two different categories (e.g., a password and a hardware token).</p>
<h3 id="historical-context-and-formalization">Historical Context and Formalization</h3>
<p>The three-factor model has its roots in the late 20th century, when computer systems and networks began handling more sensitive and valuable information. Mainframe systems of the 1960s and 1970s were already using password-based mechanisms, but as networks expanded and threats increased, the need for stronger assurances became evident.</p>
<p>In particular, the U.S. Department of Defense’s <em>Orange Book</em> (Trusted Computer System Evaluation Criteria, TCSEC, 1983) emphasized the importance of identity verification in secure systems. Later, the National Institute of Standards and Technology (NIST) published a series of guidelines (notably <strong>NIST SP 800-63</strong>) that formalized identity assurance levels (IAL), authenticator assurance levels (AAL), and recommended the use of multiple factors for high-risk scenarios. These guidelines have since influenced international standards and commercial best practices.</p>
<h3 id="authentication-vs-authorization">Authentication vs. Authorization</h3>
<p>It is also critical to distinguish <strong>authentication</strong> from <strong>authorization</strong>, two related but distinct components of access control.</p>
<ul>
<li><strong>Authentication</strong> confirms <em>who you are</em>: it is the process of establishing the identity of the entity interacting with the system.</li>
<li><strong>Authorization</strong> determines <em>what you are allowed to do</em>: it governs the permissions and access rights granted to an authenticated entity.</li>
</ul>
<p>Misunderstanding this distinction can lead to design flaws, where systems incorrectly assume that knowing <em>who</em> the user is automatically confers the right to perform certain actions, a fallacy that can open the door to privilege escalation and other attacks.</p>
<p>It is understandable to see professionals struggle with the distinction between authentication and authorization, especially when widely used standards themselves adopt misleading terminology. A notable example is the HTTP status code <code>401 Unauthorized</code>, which seems to imply an authorization failure. However, RFC 7235 clarifies that it actually indicates the absence of valid authentication credentials, in other words, the client is unauthenticated. Conversely, if the client is authenticated but lacks permission to access the resource, the appropriate status is <code>403 Forbidden</code>. Such ambiguous terminology can contribute to confusion, leading to poor error handling, security misconfigurations, and misleading diagnostics in practice.</p>
<p>Below is an expanded and more technically detailed version of your draft, preserving your structure and style while deepening the discussion:</p>
<hr>
<h2 id="the-importance-of-multi-factor-authentication"><em>The Importance of Multi-Factor Authentication</em></h2>
<h3 id="what-is-strong-authentication">What is “strong” authentication?</h3>
<p>The term <strong>strong authentication</strong> refers to an authentication mechanism that significantly raises the barrier against unauthorized access by requiring evidence from <em>multiple independent factors</em>, each of which presents distinct challenges to an attacker.</p>
<p>According to NIST SP 800-63B (“Digital Identity Guidelines”), strong authentication is achieved when authentication combines <strong>two or more different factors</strong>.</p>
<p>This taxonomy is rooted in the observation that each factor type involves a distinct trust assumption and failure mode. For example, a password can be guessed or leaked; a smartcard can be stolen; a fingerprint could theoretically be replicated, but achieving all three simultaneously is much more difficult.</p>
<p>This principle is also emphasized by regulatory frameworks such as the European Union’s <strong>Revised Payment Services Directive (PSD2)</strong>, specifically its provision on <strong>Strong Customer Authentication (SCA)</strong>, which mandates that authentication in financial services “validates the identity of the user through at least two factors which are independent from each other, and designed in such a way that the breach of one does not compromise the reliability of the other”.</p>
<p>The emphasis on <strong>independence</strong> is critical: if compromising one factor gives an attacker a pathway to compromise the second (e.g., a password reset sent to email when both accounts use the same password), the overall scheme fails to meet the standard of strong authentication.</p>
<h3 id="why-does-strong-authentication-matter">Why does strong authentication matter?</h3>
<p>Weak or single-factor authentication remains one of the most commonly exploited vulnerabilities in both consumer and enterprise environments.</p>
<p>For instance, the <strong>Verizon Data Breach Investigations Report (DBIR)</strong> consistently finds that over 80% of hacking-related breaches involve stolen, weak, or reused credentials. Similarly, Microsoft reports that enabling MFA can prevent over 99.9% of automated account compromise attacks, a figure reflecting the asymmetry between the effort needed to phish or guess a password and the effort needed to also obtain a second independent factor.</p>
<p>More granular studies (including Google’s analysis of millions of accounts) demonstrate that even lower-assurance factors, like SMS OTPs, reduce bulk phishing attack success by over 96%, and targeted attacks by over 76%. These figures improve further when higher-assurance factors (e.g., hardware security keys) are used.</p>
<p>The underlying reason is that MFA introduces a requirement that attackers compromise at least two unrelated mechanisms simultaneously: for example, intercepting a password <em>and</em> stealing a device or defeating a biometric reader. This drastically increases both the cost and complexity of attacks, often forcing attackers to move on to softer targets.</p>
<p>It also effectively neutralizes whole classes of attacks, such as credential stuffing and password spraying, which rely on automated exploitation of password reuse.</p>
<h3 id="common-misconceptions-about-mfa">Common misconceptions about MFA</h3>
<p>Despite its benefits, MFA is often misunderstood or improperly implemented, sometimes resulting in a false sense of security.</p>
<h3 id="mfa-vs-multi-step-authentication">MFA vs. multi-step authentication</h3>
<p>A frequent mistake is conflating <strong>multi-step</strong> authentication with <strong>multi-factor</strong> authentication. For example, many services send a <strong>one-time code (OTC)</strong> to a user’s email or SMS after password entry. While this adds an additional <em>step</em>, it does not necessarily add a true <em>factor</em>, because both steps may belong to the same category of authentication (typically <em>something you know</em> or <em>something you control digitally</em>).</p>
<p>Consider the following example:</p>
<ul>
<li>Step 1: Enter a password (something you know).</li>
<li>Step 2: Retrieve a code from email (also something you know and control via another password).</li>
</ul>
<p>Here, if the attacker compromises the email account, which may also be secured by the same password or an easily guessed one, both factors fall simultaneously, invalidating the intended security benefit.</p>
<p>For authentication to qualify as MFA, each factor must be drawn from a distinct category, and their compromise must be independent. Proper examples include:</p>
<ul>
<li>Password + hardware security token (knowledge + possession)</li>
<li>Password + fingerprint (knowledge + inherence)</li>
</ul>
<h3 id="the-problem-with-sms-and-email-otps">The problem with SMS and email OTPs</h3>
<p>Another misconception is that <strong>SMS or email-based OTCs are inherently secure</strong> because they add a step after password entry. While they are better than no additional step, they suffer from serious weaknesses:</p>
<ul>
<li>SMS is vulnerable to SIM-swapping, SS7 protocol attacks, and social engineering of mobile carriers.</li>
<li>Email can be compromised via phishing, password reuse, or malware, and is often the recovery vector for many accounts, making it a high-value target.</li>
</ul>
<p>A well-documented example of this is the scam that targeted <strong>O2 customers</strong> in the UK, where attackers used social engineering to extract SMS OTCs under the guise of a prize verification process. Once customers revealed the codes, attackers immediately accessed and took over the accounts.</p>
<h3 id="biometric-fallacies">Biometric fallacies</h3>
<p>Even biometric factors (<em>something you are</em>) are sometimes misused or misunderstood. People often believe that biometrics are foolproof. However:</p>
<ul>
<li>Many biometric systems are based on templates that can be stolen and replayed.</li>
<li>Some can be spoofed with photographs, silicone molds, or deepfake techniques.</li>
<li>Unlike passwords, compromised biometric data cannot be changed.</li>
</ul>
<p>Therefore, biometrics are best used as <em>one factor among others</em>, not as a standalone mechanism.</p>
<h3 id="recovery-paths-can-undermine-mfa">Recovery paths can undermine MFA</h3>
<p>Even properly designed MFA can be defeated if account recovery paths bypass it. For example, if a service allows a password reset via email without re-verifying possession of the second factor, an attacker can compromise the email account and reset the password, effectively bypassing MFA.</p>
<hr>
<h2 id="best-practices-for-stronger-authentication"><em>Best Practices for Stronger Authentication</em></h2>
<p>Multi-factor authentication (MFA) has proven itself to be one of the most effective countermeasures against unauthorized access, credential stuffing, and account takeovers. However, its effectiveness depends critically on how it is implemented and understood.</p>
<p>In the preceding sections, we examined the foundational principles of authentication, clarified the distinction between authentication and authorization, and addressed common misconceptions about what constitutes “strong” authentication. Misapplied MFA, such as using two steps from the same factor category, or relying on insecure channels like SMS or email, can create a false sense of security.</p>
<h3 id="recommendations-for-implementing-mfa-effectively">Recommendations for implementing MFA effectively</h3>
<p>To close, here are several best practices based on guidance from NIST, ENISA, and industry experience:</p>
<ul>
<li>
<p><strong>Ensure factor independence</strong>: Select factors from different categories (<em>know</em>, <em>have</em>, <em>are</em>) and ensure compromise of one does not compromise the others.</p>
</li>
<li>
<p><strong>Prefer phishing-resistant methods</strong>: Whenever possible, opt for cryptographically bound hardware authenticators (e.g., FIDO2/WebAuthn security keys) or platform-based authenticators (e.g., Windows Hello, Touch ID) that resist phishing and man-in-the-middle attacks.</p>
</li>
<li>
<p><strong>Avoid SMS and email as primary second factors</strong>: Use these channels only as fallback or recovery mechanisms, not as primary authentication factors, due to their susceptibility to interception, SIM swapping, and social engineering.</p>
</li>
<li>
<p><strong>Educate users about social engineering</strong>: Many attacks bypass MFA by tricking users into revealing one-time codes or approving fraudulent login attempts. Training users to recognize and resist such attempts is essential.</p>
</li>
<li>
<p><strong>Monitor and adapt</strong>: Treat authentication as a living component of your security architecture. Monitor for evolving threats, evaluate the security of chosen factors periodically, and adapt policies as needed.</p>
</li>
</ul>
<h3 id="final-thoughts">Final thoughts</h3>
<p>Authentication is more than a technical hurdle; it is a cornerstone of trust in digital systems. Understanding the nuances of MFA, resisting the allure of superficial “two-step” schemes, and adopting strong, independent factors are essential steps toward robust access control.</p>
<p>By grounding your implementation in the principles outlined here, independence of factors, resistance to phishing, and resilience against social engineering, you can significantly reduce the risk of unauthorized access while preserving usability.</p>
]]></content></item><item><title>Microbenchmarking Go: Bytes vs. Runes and the Hidden Cost of Map Keys</title><link>https://pessolato.github.io/posts/go-str-map-microbench/</link><pubDate>Sun, 22 Jun 2025 15:03:36 +0200</pubDate><guid>https://pessolato.github.io/posts/go-str-map-microbench/</guid><description>&lt;p>While working through the &lt;a href="https://exercism.org/tracks/go">Go track on Exercism.org&lt;/a>, I ran into an interesting performance puzzle in the &lt;strong>Nucleotide Count&lt;/strong> exercise. It looked simple at first glance, but it sent me down a rabbit hole of microbenchmarking that revealed an insight beyond just how you iterate over strings.&lt;/p>
&lt;h2 id="the-initial-question-bytes-or-runes">The Initial Question: Bytes or Runes?&lt;/h2>
&lt;p>The input is a DNA sequence, which contain only ASCII characters like &lt;code>A&lt;/code>, &lt;code>C&lt;/code>, &lt;code>G&lt;/code>, and &lt;code>T&lt;/code>. Naturally, I reached for a &lt;code>map&lt;/code> to count each nucleotide.&lt;/p></description><content type="html"><![CDATA[<p>While working through the <a href="https://exercism.org/tracks/go">Go track on Exercism.org</a>, I ran into an interesting performance puzzle in the <strong>Nucleotide Count</strong> exercise. It looked simple at first glance, but it sent me down a rabbit hole of microbenchmarking that revealed an insight beyond just how you iterate over strings.</p>
<h2 id="the-initial-question-bytes-or-runes">The Initial Question: Bytes or Runes?</h2>
<p>The input is a DNA sequence, which contain only ASCII characters like <code>A</code>, <code>C</code>, <code>G</code>, and <code>T</code>. Naturally, I reached for a <code>map</code> to count each nucleotide.</p>
<p>With that set up, I added the iteration over the string using byte indexing (<code>for i := 0; i &lt; len(s); i++</code>), since I remembered a comment on <a href="https://www.reddit.com/r/golang/">r/golang</a> suggesting that byte iteration is idiomatic and efficient for ASCII-only strings.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">dna</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;fmt&#34;</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Histogram</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">byte</span>]<span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">NewHistogram</span>() <span style="color:#a6e22e">Histogram</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">Histogram</span>{<span style="color:#e6db74">&#39;A&#39;</span>: <span style="color:#ae81ff">0</span>, <span style="color:#e6db74">&#39;C&#39;</span>: <span style="color:#ae81ff">0</span>, <span style="color:#e6db74">&#39;G&#39;</span>: <span style="color:#ae81ff">0</span>, <span style="color:#e6db74">&#39;T&#39;</span>: <span style="color:#ae81ff">0</span>}
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">DNA</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">d</span> <span style="color:#a6e22e">DNA</span>) <span style="color:#a6e22e">Counts</span>() (<span style="color:#a6e22e">Histogram</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">h</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">NewHistogram</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> &lt; len(<span style="color:#a6e22e">d</span>); <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">h</span>[<span style="color:#a6e22e">d</span>[<span style="color:#a6e22e">i</span>]]; !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;invalid nucleotide %q in DNA strand&#34;</span>, <span style="color:#a6e22e">d</span>[<span style="color:#a6e22e">i</span>])
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">h</span>[<span style="color:#a6e22e">d</span>[<span style="color:#a6e22e">i</span>]]<span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">h</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="a-surprising-benchmark-result">A Surprising Benchmark Result</h2>
<p>One of my favorite parts of Exercism is reviewing other submissions and comparing microbenchmark results. This time, I noticed that a solution by <code>bobahop</code> actually <a href="https://exercism.org/tracks/go/exercises/nucleotide-count/approaches/switch-statement">used rune iteration</a> (<code>for _, r := range s</code>) and surprisingly, outperformed mine in microbenchmarks.</p>
<p>That caught me off guard. Had I been misled by Reddit?</p>
<p>I assumed that rune iteration would be slower due to the UTF-8 decoding overhead. I also knew my version was slower than <code>bobahop</code>&rsquo;s due to using <code>map</code> lookups, while his used a <code>switch</code>, so I tested his version with byte iteration instead. Even then, his rune-based solution remained faster, just as he had noted on Exercism.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">dna</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> <span style="color:#e6db74">&#34;fmt&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Histogram</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">byte</span>]<span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">DNA</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> (
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">nucA</span> <span style="color:#66d9ef">byte</span> = <span style="color:#ae81ff">65</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">nucC</span> <span style="color:#66d9ef">byte</span> = <span style="color:#ae81ff">67</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">nucG</span> <span style="color:#66d9ef">byte</span> = <span style="color:#ae81ff">71</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">nucT</span> <span style="color:#66d9ef">byte</span> = <span style="color:#ae81ff">84</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">dna</span> <span style="color:#a6e22e">DNA</span>) <span style="color:#a6e22e">Counts</span>() (<span style="color:#a6e22e">Histogram</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">results</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">Histogram</span>{<span style="color:#a6e22e">nucA</span>: <span style="color:#ae81ff">0</span>, <span style="color:#a6e22e">nucC</span>: <span style="color:#ae81ff">0</span>, <span style="color:#a6e22e">nucG</span>: <span style="color:#ae81ff">0</span>, <span style="color:#a6e22e">nucT</span>: <span style="color:#ae81ff">0</span>}
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">length</span> <span style="color:#f92672">:=</span> len(<span style="color:#a6e22e">dna</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> &lt; <span style="color:#a6e22e">length</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">nuc</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">dna</span>[<span style="color:#a6e22e">i</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">nuc</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">nucA</span>, <span style="color:#a6e22e">nucC</span>, <span style="color:#a6e22e">nucG</span>, <span style="color:#a6e22e">nucT</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">results</span>[<span style="color:#a6e22e">nuc</span>]<span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">default</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;invalid nucleotide &#39;%c&#39;&#34;</span>, <span style="color:#a6e22e">nuc</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">results</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="my-initial-approach">My Initial Approach</h2>
<p>To get to the bottom of this, I created a small repo to isolate and benchmark different implementations using both byte and rune iteration.</p>
<p>I started with a simpler function: transcribing DNA to RNA.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TranscribeDnaToRnaBytes</span>(<span style="color:#a6e22e">dna</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">rna</span> []<span style="color:#66d9ef">byte</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> len(<span style="color:#a6e22e">dna</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">dna</span>[<span style="color:#a6e22e">i</span>] {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;A&#39;</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">rna</span>[<span style="color:#a6e22e">i</span>] = <span style="color:#e6db74">&#39;U&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;C&#39;</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">rna</span>[<span style="color:#a6e22e">i</span>] = <span style="color:#e6db74">&#39;G&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;G&#39;</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">rna</span>[<span style="color:#a6e22e">i</span>] = <span style="color:#e6db74">&#39;C&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;T&#39;</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">rna</span>[<span style="color:#a6e22e">i</span>] = <span style="color:#e6db74">&#39;A&#39;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TranscribeDnaToRnaRunes</span>(<span style="color:#a6e22e">dna</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">rna</span> []<span style="color:#66d9ef">rune</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span>, <span style="color:#a6e22e">n</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">dna</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">n</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;A&#39;</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">rna</span>[<span style="color:#a6e22e">i</span>] = <span style="color:#e6db74">&#39;U&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;C&#39;</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">rna</span>[<span style="color:#a6e22e">i</span>] = <span style="color:#e6db74">&#39;G&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;G&#39;</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">rna</span>[<span style="color:#a6e22e">i</span>] = <span style="color:#e6db74">&#39;C&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;T&#39;</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">rna</span>[<span style="color:#a6e22e">i</span>] = <span style="color:#e6db74">&#39;A&#39;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>As expected, the benchmarks showed that <strong>byte iteration was slightly faster</strong> than rune iteration.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>$ go test -bench<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;^(BenchmarkTranscribeDnaToRnaBytes|BenchmarkTranscribeDnaToRnaRunes)$&#39;</span> -benchtime<span style="color:#f92672">=</span>1000000x -benchmem
</span></span><span style="display:flex;"><span>goos: linux
</span></span><span style="display:flex;"><span>goarch: amd64
</span></span><span style="display:flex;"><span>pkg: github.com/pessolato/strmapmicrobench
</span></span><span style="display:flex;"><span>cpu: 12th Gen Intel<span style="color:#f92672">(</span>R<span style="color:#f92672">)</span> Core<span style="color:#f92672">(</span>TM<span style="color:#f92672">)</span> i9-12900F
</span></span><span style="display:flex;"><span>BenchmarkTranscribeDnaToRnaBytes-24      <span style="color:#ae81ff">1000000</span>               331.4 ns/op             <span style="color:#ae81ff">0</span> B/op          <span style="color:#ae81ff">0</span> allocs/op
</span></span><span style="display:flex;"><span>BenchmarkTranscribeDnaToRnaRunes-24      <span style="color:#ae81ff">1000000</span>               529.3 ns/op             <span style="color:#ae81ff">0</span> B/op          <span style="color:#ae81ff">0</span> allocs/op
</span></span></code></pre></div><p>Then I rewrote the nucleotide count functions, simplifying and updating them. I also used a more idiomatic byte loop (<code>for i := range s</code>) to see if that would help.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">CountNucleotidesByByteIndex</span>(<span style="color:#a6e22e">dna</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">byte</span>]<span style="color:#66d9ef">int</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">chars</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">byte</span>]<span style="color:#66d9ef">int</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> len(<span style="color:#a6e22e">dna</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">chars</span>[<span style="color:#a6e22e">dna</span>[<span style="color:#a6e22e">i</span>]]<span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">chars</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">CountNucleotidesByRuneRange</span>(<span style="color:#a6e22e">dna</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">rune</span>]<span style="color:#66d9ef">int</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">chars</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">rune</span>]<span style="color:#66d9ef">int</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">dna</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">chars</span>[<span style="color:#a6e22e">r</span>]<span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">chars</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>But the performance difference persisted.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>$ go test -bench<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;^(BenchmarkCountNucleotidesByByteIndex|BenchmarkCountNucleotidesByRuneRange)$&#39;</span> -benchtime<span style="color:#f92672">=</span>1000000x -benchmem
</span></span><span style="display:flex;"><span>goos: linux
</span></span><span style="display:flex;"><span>goarch: amd64
</span></span><span style="display:flex;"><span>pkg: github.com/pessolato/strmapmicrobench
</span></span><span style="display:flex;"><span>cpu: 12th Gen Intel<span style="color:#f92672">(</span>R<span style="color:#f92672">)</span> Core<span style="color:#f92672">(</span>TM<span style="color:#f92672">)</span> i9-12900F
</span></span><span style="display:flex;"><span>BenchmarkCountNucleotidesByByteIndex-24          <span style="color:#ae81ff">1000000</span>              <span style="color:#ae81ff">5191</span> ns/op             <span style="color:#ae81ff">192</span> B/op          <span style="color:#ae81ff">2</span> allocs/op
</span></span><span style="display:flex;"><span>BenchmarkCountNucleotidesByRuneRange-24          <span style="color:#ae81ff">1000000</span>              <span style="color:#ae81ff">1682</span> ns/op             <span style="color:#ae81ff">192</span> B/op          <span style="color:#ae81ff">2</span> allocs/op
</span></span><span style="display:flex;"><span>PASS
</span></span><span style="display:flex;"><span>ok      github.com/pessolato/strmapmicrobench   6.876s
</span></span></code></pre></div><p>At this point, I realized the bottleneck might not be the loop. So I turned to profiling.</p>
<h2 id="cpu-profiling-tells-the-real-story">CPU Profiling Tells the Real Story</h2>
<p>I profiled both versions of the counting function and things started to click.</p>
<p><img src="/posts/go-str-map-microbench/Profile-ByByteIndex-VS-ByRuneRange.png" alt="Graph of CPU Profile" title="Graph of CPU Profile"></p>
<p>The version using byte indexing was calling the generic <code>mapassign</code> function, while the rune-based version was using <code>mapassign_fast32</code>, which is much faster.</p>
<p>That was the aha moment. The real issue wasn’t string iteration, but <strong>how maps work under the hood</strong>, especially based on the <strong>key type</strong>.</p>
<h2 id="gos-internal-map-implementations">Go’s Internal Map Implementations</h2>
<p>The Go runtime uses different internal implementations for <code>map</code> operations depending on the key type:</p>
<ul>
<li>For smaller key types like <code>byte</code>, it uses the slower generic <code>mapassign</code>.</li>
<li>For <code>int32</code> (which <code>rune</code> is an alias of), it uses optimized versions like <code>mapassign_fast32</code>.</li>
</ul>
<p>So, despite rune iteration having more overhead, the faster map operations made the overall function faster.</p>
<p>To test this theory further, I added two more functions: one that uses byte iteration but casts to <code>int16</code>, and another that casts to <code>rune</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">CountNucleotidesByByteIndexAsRune</span>(<span style="color:#a6e22e">dna</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">rune</span>]<span style="color:#66d9ef">int</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">chars</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">rune</span>]<span style="color:#66d9ef">int</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> len(<span style="color:#a6e22e">dna</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">chars</span>[rune(<span style="color:#a6e22e">dna</span>[<span style="color:#a6e22e">i</span>])]<span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">chars</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">CountNucleotidesByByteIndexAsInt16</span>(<span style="color:#a6e22e">dna</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">int16</span>]<span style="color:#66d9ef">int</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">chars</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">int16</span>]<span style="color:#66d9ef">int</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> len(<span style="color:#a6e22e">dna</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">chars</span>[int16(<span style="color:#a6e22e">dna</span>[<span style="color:#a6e22e">i</span>])]<span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">chars</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Benchmark results:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>$ go test -bench<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;^BenchmarkCountNucleotidesBy.*$&#39;</span> -benchtime<span style="color:#f92672">=</span>1000000x -benchmem
</span></span><span style="display:flex;"><span>goos: linux
</span></span><span style="display:flex;"><span>goarch: amd64
</span></span><span style="display:flex;"><span>pkg: github.com/pessolato/strmapmicrobench
</span></span><span style="display:flex;"><span>cpu: 12th Gen Intel<span style="color:#f92672">(</span>R<span style="color:#f92672">)</span> Core<span style="color:#f92672">(</span>TM<span style="color:#f92672">)</span> i9-12900F
</span></span><span style="display:flex;"><span>BenchmarkCountNucleotidesByByteIndex-24                  <span style="color:#ae81ff">1000000</span>              <span style="color:#ae81ff">5177</span> ns/op             <span style="color:#ae81ff">192</span> B/op          <span style="color:#ae81ff">2</span> allocs/op
</span></span><span style="display:flex;"><span>BenchmarkCountNucleotidesByByteIndexAsRune-24            <span style="color:#ae81ff">1000000</span>              <span style="color:#ae81ff">1648</span> ns/op             <span style="color:#ae81ff">192</span> B/op          <span style="color:#ae81ff">2</span> allocs/op
</span></span><span style="display:flex;"><span>BenchmarkCountNucleotidesByByteIndexAsInt16-24           <span style="color:#ae81ff">1000000</span>              <span style="color:#ae81ff">5132</span> ns/op             <span style="color:#ae81ff">192</span> B/op          <span style="color:#ae81ff">2</span> allocs/op
</span></span><span style="display:flex;"><span>BenchmarkCountNucleotidesByRuneRange-24                  <span style="color:#ae81ff">1000000</span>              <span style="color:#ae81ff">1670</span> ns/op             <span style="color:#ae81ff">192</span> B/op          <span style="color:#ae81ff">2</span> allocs/op
</span></span><span style="display:flex;"><span>PASS
</span></span><span style="display:flex;"><span>ok      github.com/pessolato/strmapmicrobench   13.631s
</span></span></code></pre></div><p>Interestingly, <code>int16</code> didn’t get any runtime optimization. But casting bytes to <code>rune</code> actually performed better than the native rune iteration, because byte iteration is inherently faster, and <code>rune</code> still benefits from optimized map handling.</p>
<h2 id="final-optimization-arrays-and-switches">Final Optimization: Arrays and Switches</h2>
<p>Now that I understood the map overhead, I tried a more aggressive optimization. Since the nucleotide set is small and known, I replaced maps with a <strong>fixed-size array</strong> and a <code>switch</code> statement.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">CountNucleotidesArrayBytes</span>(<span style="color:#a6e22e">dna</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">byte</span>]<span style="color:#66d9ef">int</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">chars</span> <span style="color:#f92672">:=</span> [<span style="color:#ae81ff">4</span>]<span style="color:#66d9ef">int</span>{}
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> len(<span style="color:#a6e22e">dna</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">dna</span>[<span style="color:#a6e22e">i</span>] {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;A&#39;</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">chars</span>[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;C&#39;</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">chars</span>[<span style="color:#ae81ff">1</span>]<span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;G&#39;</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">chars</span>[<span style="color:#ae81ff">2</span>]<span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;T&#39;</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">chars</span>[<span style="color:#ae81ff">3</span>]<span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">byte</span>]<span style="color:#66d9ef">int</span>{
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;A&#39;</span>: <span style="color:#a6e22e">chars</span>[<span style="color:#ae81ff">0</span>],
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;C&#39;</span>: <span style="color:#a6e22e">chars</span>[<span style="color:#ae81ff">1</span>],
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;G&#39;</span>: <span style="color:#a6e22e">chars</span>[<span style="color:#ae81ff">2</span>],
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;T&#39;</span>: <span style="color:#a6e22e">chars</span>[<span style="color:#ae81ff">3</span>],
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">CountNucleotidesArrayRunes</span>(<span style="color:#a6e22e">dna</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">rune</span>]<span style="color:#66d9ef">int</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">chars</span> <span style="color:#f92672">:=</span> [<span style="color:#ae81ff">4</span>]<span style="color:#66d9ef">int</span>{}
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">dna</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">dna</span>[<span style="color:#a6e22e">i</span>] {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;A&#39;</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">chars</span>[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;C&#39;</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">chars</span>[<span style="color:#ae81ff">1</span>]<span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;G&#39;</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">chars</span>[<span style="color:#ae81ff">2</span>]<span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#e6db74">&#39;T&#39;</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">chars</span>[<span style="color:#ae81ff">3</span>]<span style="color:#f92672">++</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">rune</span>]<span style="color:#66d9ef">int</span>{
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;A&#39;</span>: <span style="color:#a6e22e">chars</span>[<span style="color:#ae81ff">0</span>],
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;C&#39;</span>: <span style="color:#a6e22e">chars</span>[<span style="color:#ae81ff">1</span>],
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;G&#39;</span>: <span style="color:#a6e22e">chars</span>[<span style="color:#ae81ff">2</span>],
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;T&#39;</span>: <span style="color:#a6e22e">chars</span>[<span style="color:#ae81ff">3</span>],
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>And the benchmarks:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>$ go test -bench<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;^(BenchmarkCountNucleotidesArray.*|BenchmarkCountNucleotidesByByteIndexAsRune)$&#39;</span> -benchtime<span style="color:#f92672">=</span>1000000x -benchmem
</span></span><span style="display:flex;"><span>goos: linux
</span></span><span style="display:flex;"><span>goarch: amd64
</span></span><span style="display:flex;"><span>pkg: github.com/pessolato/strmapmicrobench
</span></span><span style="display:flex;"><span>cpu: 12th Gen Intel<span style="color:#f92672">(</span>R<span style="color:#f92672">)</span> Core<span style="color:#f92672">(</span>TM<span style="color:#f92672">)</span> i9-12900F
</span></span><span style="display:flex;"><span>BenchmarkCountNucleotidesByByteIndexAsRune-24            <span style="color:#ae81ff">1000000</span>              <span style="color:#ae81ff">1642</span> ns/op             <span style="color:#ae81ff">192</span> B/op          <span style="color:#ae81ff">2</span> allocs/op
</span></span><span style="display:flex;"><span>BenchmarkCountNucleotidesArrayBytes-24                   <span style="color:#ae81ff">1000000</span>               412.9 ns/op           <span style="color:#ae81ff">192</span> B/op          <span style="color:#ae81ff">2</span> allocs/op
</span></span><span style="display:flex;"><span>BenchmarkCountNucleotidesArrayRunes-24                   <span style="color:#ae81ff">1000000</span>               680.5 ns/op           <span style="color:#ae81ff">192</span> B/op          <span style="color:#ae81ff">2</span> allocs/op
</span></span><span style="display:flex;"><span>PASS
</span></span><span style="display:flex;"><span>ok      github.com/pessolato/strmapmicrobench   2.739s
</span></span></code></pre></div><p>The result? <strong>Substantial performance gains</strong>, thanks to completely bypassing map lookups.</p>
<h2 id="conclusion">Conclusion</h2>
<p>This whole detour was a great reminder: <strong>Always benchmark and profile when performance matters</strong>. Micro-optimizations like using bytes over runes can help, but they pale in comparison to the deeper costs of how your data structures behave at runtime.</p>
<h3 id="key-takeaways">Key Takeaways</h3>
<ul>
<li><strong>Byte iteration is slightly faster</strong> than rune iteration.</li>
<li><strong>Map key type matters a lot</strong>—Go uses specialized fast paths for certain types.</li>
<li><strong>Profiling is essential</strong> for spotting unexpected performance bottlenecks.</li>
<li>When the key space is small and fixed, <strong>arrays + switch</strong> can outperform maps significantly.</li>
</ul>
<p>If you&rsquo;re curious, you can find all the code and benchmark results here: <a href="https://github.com/pessolato/strmapmicrobench">https://github.com/pessolato/strmapmicrobench</a></p>
]]></content></item></channel></rss>