<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://ivan-toriya.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://ivan-toriya.github.io/" rel="alternate" type="text/html" /><updated>2025-02-10T14:41:51+00:00</updated><id>https://ivan-toriya.github.io/feed.xml</id><title type="html">Blog</title><author><name>Ivan Toriya</name></author><entry><title type="html">Loop-invariant code motion</title><link href="https://ivan-toriya.github.io/2025/01/27/loop-invariant.html" rel="alternate" type="text/html" title="Loop-invariant code motion" /><published>2025-01-27T00:00:00+00:00</published><updated>2025-01-27T00:00:00+00:00</updated><id>https://ivan-toriya.github.io/2025/01/27/loop-invariant</id><content type="html" xml:base="https://ivan-toriya.github.io/2025/01/27/loop-invariant.html"><![CDATA[<p>While watching Anthony Shaw’s PyCon US <a href="https://youtu.be/YY7yJHo0M5I?t=659">talk</a> I came across “loop invariance” and the technic that compilers do called “loop-invariant code motion”. As I mostly used interpreted language (Python) and only recently started to look into compiled one (Go), it was interesting to learn what compilers can do for optimization. For me it was obvious not to put redundant computations in a hot loop, but with “loop-invariant code motion” it’s different, it doesn’t affect performance and even improves readability.</p>

<p>I decided to compare timings using Python 3.10<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> (CPython and PyPy) and Go 1.23.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">timeit</span>


<span class="k">def</span> <span class="nf">inside_loop</span><span class="p">():</span>
    <span class="n">a</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">]</span> <span class="o">*</span> <span class="mi">100</span>
    <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1000</span><span class="p">):</span>
        <span class="nb">len</span><span class="p">(</span><span class="n">a</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">outside_loop</span><span class="p">():</span>
    <span class="n">a</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">]</span> <span class="o">*</span> <span class="mi">100</span>
    <span class="n">a_len</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">a</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1000</span><span class="p">):</span>
        <span class="n">a_len</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">t1</span> <span class="o">=</span> <span class="n">timeit</span><span class="p">.</span><span class="n">timeit</span><span class="p">(</span><span class="s">"inside_loop()"</span><span class="p">,</span> <span class="n">number</span><span class="o">=</span><span class="mi">1_000_000</span><span class="p">,</span> <span class="nb">globals</span><span class="o">=</span><span class="nb">globals</span><span class="p">())</span>
    <span class="n">t2</span> <span class="o">=</span> <span class="n">timeit</span><span class="p">.</span><span class="n">timeit</span><span class="p">(</span><span class="s">"outside_loop()"</span><span class="p">,</span> <span class="n">number</span><span class="o">=</span><span class="mi">1_000_000</span><span class="p">,</span> <span class="nb">globals</span><span class="o">=</span><span class="nb">globals</span><span class="p">())</span>

    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"len() inside loop: </span><span class="si">{</span><span class="n">t1</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s">s"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"len() outside the loop: </span><span class="si">{</span><span class="n">t2</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s">s"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"outside_loop() is faster than inside_loop() by a factor of </span><span class="si">{</span><span class="n">t1</span> <span class="o">/</span> <span class="n">t2</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>

</code></pre></div></div>

<p>Running with CPython</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ uv run --python cpython-3.10 ./python/loop_invar.py 

len() inside loop: 54.34s
len() outside the loop: 22.28s
outside_loop() is faster than inside_loop() by a factor of 2.44
</code></pre></div></div>

<p>And with PyPy</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ uv run --python pypy-3.10 ./python/loop_invar.py 

len() inside loop: 1.10s
len() outside the loop: 0.70s
outside_loop() is faster than inside_loop() by a factor of 1.57
</code></pre></div></div>

<p>So for CPython, it’s ~144% faster, and for PyPy, it’s ~57% faster. But look at the absolute time, the PyPy is incredibly more performant. I’m not really sure that the “loop-invariant code motion” is actually applied in the PyPy implementation at all, as the difference is still quite substantial. So, I attribute the speedup due to general PyPy optimizations.</p>

<p>But now, let’s look how Go compiler handles it.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// main.go</span>

<span class="k">package</span> <span class="n">main</span>

<span class="k">func</span> <span class="n">insideLoop</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">a</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">int</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">500</span><span class="p">)</span>
	<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="m">100</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
		<span class="n">a</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="m">1</span><span class="p">,</span> <span class="m">2</span><span class="p">,</span> <span class="m">3</span><span class="p">,</span> <span class="m">4</span><span class="p">,</span> <span class="m">5</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="m">100</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
		<span class="n">_</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">a</span><span class="p">)</span>
	<span class="p">}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">outsideLoop</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">a</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">int</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="m">500</span><span class="p">)</span>
	<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="m">100</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
		<span class="n">a</span> <span class="o">=</span> <span class="nb">append</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="m">1</span><span class="p">,</span> <span class="m">2</span><span class="p">,</span> <span class="m">3</span><span class="p">,</span> <span class="m">4</span><span class="p">,</span> <span class="m">5</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="n">aLen</span> <span class="o">:=</span> <span class="nb">len</span><span class="p">(</span><span class="n">a</span><span class="p">)</span>
	<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="m">100</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
		<span class="n">_</span> <span class="o">=</span> <span class="n">aLen</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// main_test.go</span>

<span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="s">"testing"</span>

<span class="k">func</span> <span class="n">BenchmarkInsideLoop</span><span class="p">(</span><span class="n">b</span> <span class="o">*</span><span class="n">testing</span><span class="o">.</span><span class="n">B</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">b</span><span class="o">.</span><span class="n">N</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
		<span class="n">insideLoop</span><span class="p">()</span>
	<span class="p">}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">BenchmarkOutsideLoop</span><span class="p">(</span><span class="n">b</span> <span class="o">*</span><span class="n">testing</span><span class="o">.</span><span class="n">B</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">for</span> <span class="n">i</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">b</span><span class="o">.</span><span class="n">N</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span> <span class="p">{</span>
		<span class="n">outsideLoop</span><span class="p">()</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Running the Go benchmark</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>go <span class="nb">test</span> <span class="nt">-bench</span><span class="o">=</span><span class="nb">.</span> <span class="nt">-benchtime</span><span class="o">=</span>1000000x

goos: linux
goarch: amd64
pkg: loopinvariant
cpu: Intel<span class="o">(</span>R<span class="o">)</span> Core<span class="o">(</span>TM<span class="o">)</span> i7-8565U CPU @ 1.80GHz
BenchmarkInsideLoop-8            1000000               428.4 ns/op
BenchmarkOutsideLoop-8           1000000               438.8 ns/op
PASS
ok      loopinvariant   0.870s
</code></pre></div></div>

<p>We can see that the difference is much more subtle. OutsideLoop is faster than InsideLoop by a factor of (428.4ns / 438.8ns) = 0.98. InsideLoop is even faster, but let’s attribute it to external factors and unknowns which I don’t know.</p>

<p>On the plot below, we can see the difference between each implementation.</p>

<p><img src="https://ivan-toriya.github.io/assets/images/2025-01-27-loop-invariant/performance_comparison.png" alt="Performance Comparison plot" /></p>

<p>For interpreted languages like Python, moving invariant computations outside the loop can have a significant impact on performance. But compiled languages like Go benefit from “loop-invariant code motion”, resulting in no particular differences between the two implementations.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Using 3.10, as this is the latest PyPy version at the moment. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Ivan Toriya</name></author><summary type="html"><![CDATA[While watching Anthony Shaw’s PyCon US talk I came across “loop invariance” and the technic that compilers do called “loop-invariant code motion”. As I mostly used interpreted language (Python) and only recently started to look into compiled one (Go), it was interesting to learn what compilers can do for optimization. For me it was obvious not to put redundant computations in a hot loop, but with “loop-invariant code motion” it’s different, it doesn’t affect performance and even improves readability.]]></summary></entry></feed>