| CARVIEW |
Select Language
HTTP/2 200
server: nginx/1.24.0
date: Fri, 16 Jan 2026 00:15:52 GMT
content-type: text/xml; charset=utf-8
last-modified: Sat, 13 Dec 2025 07:24:24 GMT
vary: Accept-Encoding
etag: W/"693d14a8-10f96"
expires: Fri, 16 Jan 2026 00:25:52 GMT
cache-control: max-age=600
strict-transport-security: max-age=31536000
content-encoding: gzip
Roman Cheplyaka
https://ro-che.info/articles/
Articles by Roman Cheplyaka
-
StateT vs. IORef: a benchmark
<p>Sometimes I’m writing an IO loop in Haskell, and I need some sort of
a counter or accumulator. The two main options are to use a mutable
reference (IORef) or to put a StateT transformer on top the IO
monad.</p>
<p>I was curious, though, if there was a difference in efficiency
between these two approaches. Intuitively, IORefs are dedicated heap
objects, while a StateT transformer’s state becomes “just” a local
variable, so StateT might optimize better. But how much of a difference
does it make?</p>
<p>So I benchmarked the four functions, all of which calculate the sum
of numbers between 1 and <code>n = 1000</code>.</p>
<p><code>base_sum</code> simply calls <code>sum</code> from the base
package; <code>state_sum</code> and <code>stateT_sum</code> maintain the
accumulator using the <code>State Int</code> and
<code>StateT Int IO</code> monads, respectively, and
<code>ioref_sum</code> uses an <code>IORef</code> within the
<code>IO</code> monad. And here are the results, as reported by
criterion.</p>
<figure>
<img src="/img/StateT-vs-IORef.svg"
alt="Mean execution times reported by criterion. The error bars are the lower and upper bounds of the mean as reported by criterion, which I think are 95% bootstrap confidence intervals." />
<figcaption aria-hidden="true">Mean execution times reported by
criterion. The error bars are the lower and upper bounds of the mean as
reported by criterion, which I think are 95% bootstrap confidence
intervals.</figcaption>
</figure>
<p>I’m not sure how <code>stateT_sum</code> manages to be faster than
<code>state_sum</code> and <code>base_sum</code> (this doesn’t appear to
be a statistical fluke), but what’s clear is that <code>ioref_sum</code>
is significantly slower of them all.</p>
<p>So if 3ns per state access matter to you, go for <code>StateT</code>
even when you are in <code>IO</code>.</p>
<p>(Update: also check out the <a
href="https://old.reddit.com/r/haskell/comments/knne96/statet_vs_ioref_a_benchmark/">comments
on reddit</a>, especially the ones by u/VincentPepper.)</p>
<p>Here’s the full benchmark code. It was compiled with <code>-O2</code>
by GHC 8.8.4 and run on AMD Ryzen 7 3700X.</p>
<div class="sourceCode" id="cb1"><pre
class="sourceCode haskell"><code class="sourceCode haskell"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="kw">import</span> <span class="dt">Criterion</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="kw">import</span> <span class="dt">Criterion.Main</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a><span class="kw">import</span> <span class="dt">Control.Monad.State</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a><span class="kw">import</span> <span class="dt">Data.IORef</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a><span class="ot">base_sum ::</span> <span class="dt">Int</span> <span class="ot">-></span> <span class="dt">Int</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>base_sum n <span class="ot">=</span> <span class="fu">sum</span> [<span class="dv">1</span> <span class="op">..</span> n]</span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a><span class="ot">state_sum ::</span> <span class="dt">Int</span> <span class="ot">-></span> <span class="dt">Int</span></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a>state_sum n <span class="ot">=</span> <span class="fu">flip</span> execState <span class="dv">0</span> <span class="op">$</span></span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a> forM_ [<span class="dv">1</span><span class="op">..</span>n] <span class="op">$</span> \i <span class="ot">-></span></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a> modify' (<span class="op">+</span>i)</span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a><span class="ot">stateT_sum ::</span> <span class="dt">Int</span> <span class="ot">-></span> <span class="dt">IO</span> <span class="dt">Int</span></span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a>stateT_sum n <span class="ot">=</span> <span class="fu">flip</span> execStateT <span class="dv">0</span> <span class="op">$</span></span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a> forM_ [<span class="dv">1</span><span class="op">..</span>n] <span class="op">$</span> \i <span class="ot">-></span></span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a> modify' (<span class="op">+</span>i)</span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-20"><a href="#cb1-20" aria-hidden="true" tabindex="-1"></a><span class="ot">ioref_sum ::</span> <span class="dt">Int</span> <span class="ot">-></span> <span class="dt">IO</span> <span class="dt">Int</span></span>
<span id="cb1-21"><a href="#cb1-21" aria-hidden="true" tabindex="-1"></a>ioref_sum n <span class="ot">=</span> <span class="kw">do</span></span>
<span id="cb1-22"><a href="#cb1-22" aria-hidden="true" tabindex="-1"></a> ref <span class="ot"><-</span> newIORef <span class="dv">0</span></span>
<span id="cb1-23"><a href="#cb1-23" aria-hidden="true" tabindex="-1"></a> forM_ [<span class="dv">1</span><span class="op">..</span>n] <span class="op">$</span> \i <span class="ot">-></span></span>
<span id="cb1-24"><a href="#cb1-24" aria-hidden="true" tabindex="-1"></a> modifyIORef' ref (<span class="op">+</span>i)</span>
<span id="cb1-25"><a href="#cb1-25" aria-hidden="true" tabindex="-1"></a> readIORef ref</span>
<span id="cb1-26"><a href="#cb1-26" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-27"><a href="#cb1-27" aria-hidden="true" tabindex="-1"></a>main <span class="ot">=</span> <span class="kw">do</span></span>
<span id="cb1-28"><a href="#cb1-28" aria-hidden="true" tabindex="-1"></a> <span class="kw">let</span> n <span class="ot">=</span> <span class="dv">1000</span></span>
<span id="cb1-29"><a href="#cb1-29" aria-hidden="true" tabindex="-1"></a> defaultMain</span>
<span id="cb1-30"><a href="#cb1-30" aria-hidden="true" tabindex="-1"></a> [ bench <span class="st">"base_sum"</span> <span class="op">$</span> whnf base_sum n</span>
<span id="cb1-31"><a href="#cb1-31" aria-hidden="true" tabindex="-1"></a> , bench <span class="st">"state_sum"</span> <span class="op">$</span> whnf state_sum n</span>
<span id="cb1-32"><a href="#cb1-32" aria-hidden="true" tabindex="-1"></a> , bench <span class="st">"stateT_sum"</span> <span class="op">$</span> whnfAppIO stateT_sum n</span>
<span id="cb1-33"><a href="#cb1-33" aria-hidden="true" tabindex="-1"></a> , bench <span class="st">"ioref_sum"</span> <span class="op">$</span> whnfAppIO ioref_sum n</span>
<span id="cb1-34"><a href="#cb1-34" aria-hidden="true" tabindex="-1"></a> ]</span></code></pre></div>
Tue, 29 Dec 2020 20:00:00 +0000
https://ro-che.info/articles/2020-12-29-statet-vs-ioref
https://ro-che.info//articles/2020-12-29-statet-vs-ioref.html
-
Laptop vs. desktop for compiling Haskell code
<p>I’ve been using various laptops as daily drivers for the last 12
years, and I’ve never felt they were inadequate — until this year. There
were a few things that made me put together a desktop PC last month, but
a big reason was to improve my Haskell compilation experience on big
projects.</p>
<p>So let’s test how fast Haskell code compiles on a laptop vs. a
desktop.</p>
<h2 id="specs">Specs</h2>
<table>
<thead>
<tr>
<th></th>
<th style="text-align: right;">Laptop</th>
<th style="text-align: right;">Desktop</th>
</tr>
</thead>
<tbody>
<tr>
<td>CPU</td>
<td style="text-align: right;">Intel Core i7-6500U</td>
<td style="text-align: right;">AMD Ryzen 7 3700X</td>
</tr>
<tr>
<td>Base clock</td>
<td style="text-align: right;">2.5 GHz</td>
<td style="text-align: right;">3.6 GHz</td>
</tr>
<tr>
<td>Boost clock</td>
<td style="text-align: right;">3.1 GHz</td>
<td style="text-align: right;">4.4 GHz</td>
</tr>
<tr>
<td>Number of cores</td>
<td style="text-align: right;">2</td>
<td style="text-align: right;">8</td>
</tr>
<tr>
<td>Memory speed</td>
<td style="text-align: right;">2133 MT/s</td>
<td style="text-align: right;">4000 MT/s</td>
</tr>
</tbody>
</table>
<h2 id="methodology">Methodology</h2>
<p>I picked four Haskell packages for this test: pandoc, lens, hledger,
and criterion. An individual test consists of building one of these
packages or all of them together (represented here by a meta-package
called <code>all</code>).</p>
<p>The build time includes the time to build all of the transitive
dependencies. All sources are pre-downloaded, so just the compilation is
timed.</p>
<p>The compilation is done using stack (current master with <a
href="https://github.com/commercialhaskell/stack/issues/5435#issuecomment-749036479">a
custom patch</a>), GHC 8.8.4, and the lts-16.26 Stackage snapshot, with
the default flags.</p>
<p>The build time of each package (including the <code>all</code>
meta-package) is measured 3 times, with all tests happening in a random
order. There is a 2 minute break after each build to let the CPU cool
down.</p>
<p>The CPU frequency governor is set to <code>performance</code> while
compiling and to <code>powersave</code> during the cooling breaks.</p>
<p>To calculate the average level of parallelism achieved on each
package, I divide the user CPU time by the wall-clock time (as reported
by GNU time’s <code>%U</code> and <code>%e</code>, respectively), using
the data from the desktop benchmark (as it has more potential for
parallelism).</p>
<p>The full benchmark script is available <a
href="/files/2020-12-22-haskell-compilation-laptop-desktop/benchmark-compilation-time.sh">here</a>.</p>
<p>I also measured the average power drawn by both computers, both when
running the benchmark and in the idle state. As my power meter only
reports the instantaneous power and cumulative energy, I measured the
cumulative energy (in W⋅h) at several random time points and fitted an
ordinary least squares linear regression to find the average power.</p>
<h2 id="results">Results</h2>
<p>The first result is that I had to take the laptop outside the house
(0°C) to even be able to finish this benchmark; otherwise the computer
would overheat and shut down. While the laptop was outside, the CPU
temperature would rise up to 74°C. The desktop, on the other hand, had
no issue keeping itself cool (< 60°C) under the room temperature with
only the stock coolers.</p>
<p>And here are the timings.</p>
<!-- html table generated in R 4.0.3 by xtable 1.8-4 package -->
<!-- Tue Dec 22 17:50:44 2020 -->
<table>
<caption align="bottom">
Mean compile times (minutes:seconds) and their ratio
</caption>
<tr>
<th>
package
</th>
<th>
desktop
</th>
<th>
laptop
</th>
<th>
ratio
</th>
</tr>
<tr>
<td>
lens
</td>
<td align="right">
01:50
</td>
<td align="right">
02:53
</td>
<td align="right">
1.57
</td>
</tr>
<tr>
<td>
criterion
</td>
<td align="right">
03:49
</td>
<td align="right">
06:05
</td>
<td align="right">
1.59
</td>
</tr>
<tr>
<td>
hledger
</td>
<td align="right">
04:28
</td>
<td align="right">
07:51
</td>
<td align="right">
1.75
</td>
</tr>
<tr>
<td>
pandoc
</td>
<td align="right">
14:07
</td>
<td align="right">
22:30
</td>
<td align="right">
1.59
</td>
</tr>
<tr>
<td>
all
</td>
<td align="right">
15:20
</td>
<td align="right">
26:48
</td>
<td align="right">
1.75
</td>
</tr>
</table>
<figure>
<img src="/img/haskell-compilation-laptop-desktop/timings.svg"
alt="The column height represents the mean time, and the error bars (which collapse into thick black lines) show the maximum and minimum of the 3 runs" />
<figcaption aria-hidden="true">The column height represents the mean
time, and the error bars (which collapse into thick black lines) show
the maximum and minimum of the 3 runs</figcaption>
</figure>
<p>We can also see how well the desktop/laptop speed ratio is predicted
by the parallelism achieved for each package.</p>
<p><img
src="/img/haskell-compilation-laptop-desktop/timings-vs-parallelism.svg" /></p>
<p>The average power (where averaging also includes the cooling breaks)
drawn during the benchmark was 19W for the laptop and 65W for the
desktop.</p>
<p>The average idle power was 3W for the laptop and 37W for the
desktop.</p>
<h2 id="conclusions">Conclusions</h2>
<ol type="1">
<li><p>The overheating laptop issue is real and has happened to me
numerous times while working on real projects, forcing me to limit the
number of threads and making the compilation even slower. This alone was
worth getting a desktop PC.</p></li>
<li><p>There’s a decent increase in the compilation speed, but it’s not
huge. The average time ratio (1.65) is much closer to the ratio of clock
frequencies (1.42–1.44) than to the difference in the combined power of
all cores. Also, the laptop/desktop ratio grows slowly with the level of
parallelism. My interpretation of this is that the (dual-core, 4
threads) laptop is capable of exploiting most of the parallelism
available when building these packages.</p>
<p>So the way things are today, I’d say a quad-core or probably even a
dual-core CPU is enough for a Haskell developer to compile code.</p>
<p>That said, I hope that our build systems become better at parallelism
over the coming years.</p></li>
<li><p>In terms of power efficiency, the laptop is a clear winner: twice
as power-efficient for compilation (after adjusting for the speed
difference) and 13 times as power-efficient when idle.
<!-- NB: the ratios were computed using more precision than reported above,
hence the results may appear wrong, but they aren't --></p></li>
<li><p>I also played a bit with overclocking the desktop’s CPU. I’m not
an experienced overclocker and didn’t dare to go to the extreme
settings, but moderate overclocking (raising the clock speed to 3.8 GHz
or enabling MSI Game Boost) actually resulted in longer compile times.
My understanding is that overclocking affects all cores, while CPU’s
default “boosting” logic (which is disabled by overclocking) can
significantly boost the clock frequency of one or two cores when needed.
The latter seems to be a much better fit for a compilation workload,
where most of the cores are idle most of the time.</p></li>
</ol>
<h2 id="acknowledgments">Acknowledgments</h2>
<p>Thanks to Félix Baylac-Jacqué for educating me about the modern PC
parts.</p>
Tue, 22 Dec 2020 20:00:00 +0000
https://ro-che.info/articles/2020-12-22-haskell-compilation-laptop-desktop
https://ro-che.info//articles/2020-12-22-haskell-compilation-laptop-desktop.html
-
How I integrate ghcid with vim/neovim
<p><a href="https://github.com/ndmitchell/ghcid">ghcid</a> by Neil
Mitchell is a simple but robust tool to get instant error messages for
your Haskell code.</p>
<p>For the most part, it doesn’t require any integration with your
editor or IDE, which is exactly what makes it robust—if you can run
ghci, you can run ghcid. There’s one feature though for which the editor
and ghcid have to talk to one another: the ability to quickly jump to
the location of the error.</p>
<p>The “official” way to integrate ghcid with neovim is <a
href="https://github.com/ndmitchell/ghcid/tree/master/plugins/nvim">the
plugin</a>. However, the plugin insists on running ghcid from within
nvim, which makes the whole thing less robust. For instance, I often
need to run ghci/ghcid in a different environment than my editor, like
in a nix shell or a docker container.</p>
<p>Therefore, I use a simpler, plugin-less setup. After all, vim/nvim
already have a feature to read the compiler output, called <a
href="https://neovim.io/doc/user/quickfix.html">quickfix</a>, and ghcid
is able to write ghci’s output to a file. All we need is a few tweaks to
make them play well together. This article describes the setup, which
I’ve been happily using for 1.5 years now.</p>
<h2 id="ghcid-setup">ghcid setup</h2>
<p>ghcid passes some flags to ghci which makes its output a bit harder
to parse.</p>
<p>Therefore, I build a modified version of ghcid, with a different
default set of flags.</p>
<p>(There are probably ways to achieve this that do not require
recompiling ghcid, but this is what I prefer—so that when I run ghcid,
it simply does what I want.)</p>
<p>The patch you need to apply is very simple:</p>
<div class="sourceCode" id="cb1"><pre
class="sourceCode diff"><code class="sourceCode diff"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="kw">--- src/Ghcid.hs</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="dt">+++ src/Ghcid.hs</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="dt">@@ -97,7 +97,7 @@ options = cmdArgsMode $ Options</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a> ,restart = [] &= typ "PATH" &= help "Restart the command when the given file or directory contents change (defaults to .ghci and any .cabal file, unless when using stack or a custom command)"</span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a> ,reload = [] &= typ "PATH" &= help "Reload when the given file or directory contents change (defaults to none)"</span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a> ,directory = "." &= typDir &= name "C" &= help "Set the current directory"</span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a><span class="st">- ,outputfile = [] &= typFile &= name "o" &= help "File to write the full output to"</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a><span class="va">+ ,outputfile = ["quickfix"] &= typFile &= name "o" &= help "File to write the full output to"</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a> ,ignoreLoaded = False &= explicit &= name "ignore-loaded" &= help "Keep going if no files are loaded. Requires --reload to be set."</span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a> ,poll = Nothing &= typ "SECONDS" &= opt "0.1" &= explicit &= name "poll" &= help "Use polling every N seconds (defaults to using notifiers)"</span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a> ,max_messages = Nothing &= name "n" &= help "Maximum number of messages to print"</span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a><span class="kw">--- src/Language/Haskell/Ghcid/Util.hs</span></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a><span class="dt">+++ src/Language/Haskell/Ghcid/Util.hs</span></span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a><span class="dt">@@ -47,7 +47,8 @@ ghciFlagsRequiredVersioned =</span></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a> -- | Flags that make ghcid work better and are supported on all GHC versions</span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a> ghciFlagsUseful :: [String]</span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a> ghciFlagsUseful =</span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a><span class="st">- ["-ferror-spans" -- see #148</span></span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a><span class="va">+ ["-fno-error-spans"</span></span>
<span id="cb1-20"><a href="#cb1-20" aria-hidden="true" tabindex="-1"></a><span class="va">+ ,"-fno-diagnostics-show-caret"</span></span>
<span id="cb1-21"><a href="#cb1-21" aria-hidden="true" tabindex="-1"></a> ,"-j" -- see #153, GHC 7.8 and above, but that's all I support anyway</span>
<span id="cb1-22"><a href="#cb1-22" aria-hidden="true" tabindex="-1"></a> ]</span></code></pre></div>
<p>Alternatively, you can clone my fork of ghcid at <a
href="https://github.com/UnkindPartition/ghcid"
class="uri">https://github.com/UnkindPartition/ghcid</a>, which already
contains the patch.</p>
<p>Apart from changing the default flags passed to ghci, it also tells
ghcid to write the ghci output to the file called <code>quickfix</code>
by default, so that you don’t have to write <code>-o quickfix</code> on
the command line every time.</p>
<h2 id="vimneovim-setup">vim/neovim setup</h2>
<p>Here are the vim pieces that you’ll need to put into your
<code>.vimrc</code> or <code>init.vim</code>. First, set the
<code>errorformat</code> option to tell vim how to parse ghci’s error
messages:</p>
<pre class="viml"><code>set errorformat=%C%*\\s•\ %m,
\%-C\ %.%#,
\%A%f:%l:%c:\ %t%.%#</code></pre>
<p>Don’t ask me how it works—it’s been a long time since I wrote it—but
it works.</p>
<p>Next, I prefer to define a few keybindings that make quickfix’ing
easier:</p>
<pre class="viml"><code>map <F5> :cfile quickfix<CR>
map <C-j> :cnext<CR>
map <C-k> :cprevious<CR></code></pre>
<p>When I see any errors in the ghcid window, I press <code>F5</code> to
load them into vim and jump to the first error. Then, if I need to, I
use Ctrl-j and Ctrl-k to jump between different errors.</p>
Wed, 08 Jul 2020 20:00:00 +0000
https://ro-che.info/articles/2020-07-08-integrate-ghcid-vim
https://ro-che.info//articles/2020-07-08-integrate-ghcid-vim.html
-
Visualizing Haskell heap profiles in 2020
<p>Heap profiling is a feature of the Glasgow Haskell Compiler (GHC)
that lets a program record its own memory usage by type, module, cost
center, or other attribute, and write it to a <code>program.hp</code>
file.</p>
<p>Here I review the existing tools—and introduce a new one—for
visualizing and analyzing these profiles.</p>
<h2 id="hp2ps">hp2ps</h2>
<p>hp2ps is the standard heap profile visualizer, as it comes bundled
with GHC.</p>
<p>Run it as</p>
<pre><code>hp2ps -c benchmark.hp</code></pre>
<p>(where <code>-c</code> makes the output colored), and it will produce
the file <code>benchmark.ps</code>, which you can open with many
document viewers.</p>
<p>Here’s what the output looks like:</p>
<figure>
<img src="/img/hp2ps.svg" alt="An example graph produced by hp2ps" />
<figcaption aria-hidden="true">An example graph produced by
hp2ps</figcaption>
</figure>
<p>The example shows a heap profile by the <em>cost center</em> stack
that allocated the data. As I mentioned, there are many other types of
heap profiles, but this is what I’ll be using here as an example.</p>
<p>As you see, the cost centers on the right are truncated. I usually
like to see them longer. They are actually truncated by the profiled
program itself, not by the visualizer, so to get longer profiles, rerun
your program with <code>+RTS -hc -L500</code> to increase the maximum
length from the default 25 to, say, 500.</p>
<p>However, hp2ps doesn’t deal well with long cost center stacks (or
other long identifiers) by default: the whole page would be filled with
identifiers, and there would be no room left for the graph itself. To
work around that, pass <code>-M</code> to hp2ps. It produces a two-page
.ps file, with the legend on the first page and the graph on the second
one.</p>
<p>I found that viewers like Okular and Evince only display the second
page of the two-page .ps file, but it works if you first convert the
output to pdf with ps2pdf. Here’s what the output looks like:</p>
<figure>
<img src="/img/hp2ps-page1.svg" /> <img src="/img/hp2ps-page2.svg" />
<figcaption>
Two-page output from hp2ps -M
</figcaption>
</figure>
<h2 id="hp2pretty">hp2pretty</h2>
<p>hp2pretty by Claude Heiland-Allen has a few advantages over hp2ps: a
nicer output with transparency and grid lines, truncation of long cost
center stacks, and the ability to write the full cost center stacks to a
file using a <code>--key</code> option.</p>
<p>Run it simply as</p>
<pre><code>hp2pretty benchmark.hp</code></pre>
<p>and it will produce a file named <code>benchmark.svg</code>.</p>
<figure>
<img src="/img/hp2pretty.svg" alt="Example output of hp2pretty" />
<figcaption aria-hidden="true">Example output of hp2pretty</figcaption>
</figure>
<h2 id="hpd3.js">hp/D3.js</h2>
<p>hp/D3.js by Edward Z. Yang is an online tool to visualize Haskell
heap profiles. There’s a hosted version at <a
href="https://heap.ezyang.com/">heap.ezyang.com</a>, and there is <a
href="https://github.com/ezyang/hpd3js">the source code</a> on
GitHub.</p>
<p>I wasn’t able to build the source code due to the dependency on
hp2any (see below), but the hosted version still works. The disadvantage
of the hosted version is that you have to upload your heap profile to
the server, and it becomes public—consider this when working on
proprietary projects. (The profile files do not contain any source code,
but even the function names and call stacks may reveal too much
information in some cases.)</p>
<p>hp/D3.js offers a choice of three different styles of pretty graphs
shown below. You can also <a
href="https://heap.ezyang.com/view/f267b68e008f3f5cc64a088106f8882ec8f097c6">browse
this profile</a> yourself. There are some cool interactive features,
like the entry’s name or call stack being highlighted when you hover the
corresponding part of the graph.</p>
<figure>
<img src="/img/hpd3js-1.png" alt="hp/d3.js: stacked graph" />
<figcaption aria-hidden="true">hp/d3.js: stacked graph</figcaption>
</figure>
<figure>
<img src="/img/hpd3js-2.png" alt="hp/d3.js: normalized stacked graph" />
<figcaption aria-hidden="true">hp/d3.js: normalized stacked
graph</figcaption>
</figure>
<figure>
<img src="/img/hpd3js-3.png" alt="hp/d3.js: overlayed area graph" />
<figcaption aria-hidden="true">hp/d3.js: overlayed area
graph</figcaption>
</figure>
<h2 id="perl-r">Perl & R</h2>
<p>Sometimes a quick look at the heap profile graph is all you need to
understand what to do next. Other times, a more detailed analysis is
required. In such cases, my favorite way is to convert an .hp file to
csv and load it into R.</p>
<p>To convert an .hp file to csv, I wrote a short Perl script, <a
href="/files/2020-05-14-visualize-haskell-heap-profiles/hp2csv">hp2csv</a>.
(Unlike many tools written in Haskell, there’s a good chance it’ll
continue working in 10 years.) Put it somewhere in your PATH, make it
executable (<code>chmod +x ~/bin/hp2csv</code>), and run</p>
<pre><code>hp2csv benchmark.hp > benchmark.csv</code></pre>
<p>The CSV has a simple format:</p>
<pre><code>time,name,value
0.094997,(487)getElements/CAF:getElements,40
0.094997,(415)CAF:$cfoldl'_r3hK,32
0.094997,(412)CAF:$ctoList_r3hH,32
0.094997,(482)match/main/Main.CAF,24
0.094997,(480)main/Main.CAF,32</code></pre>
<p>where <code>time</code> is the time in seconds since the program
start, <code>name</code> is the name of the cost center/type/etc.
(depending on what kind of heap profiling you did), and
<code>value</code> is the number of bytes.</p>
<p>Now let’s load this into R and try to reproduce the above graphs
using ggplot.</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="fu">library</span>(tidyverse)</span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="fu">library</span>(scales) <span class="co"># for a somewhat better color scheme</span></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a>csv <span class="ot"><-</span> <span class="fu">read_csv</span>(<span class="st">"benchmark.csv"</span>) <span class="sc">%>%</span></span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a> <span class="co"># convert bytes to megabytes</span></span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a> <span class="fu">mutate</span>(<span class="at">value =</span> value <span class="sc">/</span> <span class="fl">1e6</span>) <span class="sc">%>%</span></span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a> <span class="co"># absent measurements are 0s</span></span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a> <span class="fu">complete</span>(time,name, <span class="at">fill =</span> <span class="fu">list</span>(<span class="at">value =</span> <span class="dv">0</span>))</span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a> </span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a><span class="co"># find top 15 entries and sort them</span></span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a>top_names <span class="ot"><-</span> csv <span class="sc">%>%</span></span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a> <span class="fu">group_by</span>(name) <span class="sc">%>%</span></span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a> <span class="fu">summarize</span>(<span class="at">sum_value =</span> <span class="fu">sum</span>(value)) <span class="sc">%>%</span></span>
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a> <span class="fu">arrange</span>(<span class="fu">desc</span>(sum_value)) <span class="sc">%>%</span></span>
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a> <span class="fu">head</span>(<span class="at">n=</span><span class="dv">15</span>) <span class="sc">%>%</span></span>
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a> <span class="fu">mutate</span>(<span class="at">name_sorted =</span> <span class="fu">str_trunc</span>(name,<span class="dv">30</span>),</span>
<span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a> <span class="at">name_sorted =</span> <span class="fu">factor</span>(name_sorted, <span class="at">levels=</span>name_sorted))</span>
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a>top_entries <span class="ot"><-</span></span>
<span id="cb5-20"><a href="#cb5-20" aria-hidden="true" tabindex="-1"></a> <span class="fu">inner_join</span>(csv, top_names, <span class="at">by=</span><span class="st">"name"</span>)</span>
<span id="cb5-21"><a href="#cb5-21" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-22"><a href="#cb5-22" aria-hidden="true" tabindex="-1"></a><span class="co"># Create a custom color palette based on the 'viridis' palette.</span></span>
<span id="cb5-23"><a href="#cb5-23" aria-hidden="true" tabindex="-1"></a><span class="co"># Use 'sample' to shuffle the colors,</span></span>
<span id="cb5-24"><a href="#cb5-24" aria-hidden="true" tabindex="-1"></a><span class="co"># so that adjacent areas are not similarly colored.</span></span>
<span id="cb5-25"><a href="#cb5-25" aria-hidden="true" tabindex="-1"></a>colors <span class="ot"><-</span> <span class="cf">function</span>(n) {</span>
<span id="cb5-26"><a href="#cb5-26" aria-hidden="true" tabindex="-1"></a> <span class="fu">set.seed</span>(<span class="dv">2020</span>)</span>
<span id="cb5-27"><a href="#cb5-27" aria-hidden="true" tabindex="-1"></a> <span class="fu">sample</span>(<span class="fu">viridis_pal</span>(<span class="at">option=</span><span class="st">"A"</span>,<span class="at">alpha=</span><span class="fl">0.7</span>)(n))</span>
<span id="cb5-28"><a href="#cb5-28" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb5-29"><a href="#cb5-29" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-30"><a href="#cb5-30" aria-hidden="true" tabindex="-1"></a><span class="fu">theme_set</span>(<span class="fu">theme_bw</span>())</span>
<span id="cb5-31"><a href="#cb5-31" aria-hidden="true" tabindex="-1"></a><span class="fu">ggplot</span>(top_entries,<span class="fu">aes</span>(time,value,<span class="at">fill=</span>name_sorted)) <span class="sc">+</span></span>
<span id="cb5-32"><a href="#cb5-32" aria-hidden="true" tabindex="-1"></a> <span class="fu">geom_area</span>(<span class="at">position=</span><span class="st">"stack"</span>) <span class="sc">+</span></span>
<span id="cb5-33"><a href="#cb5-33" aria-hidden="true" tabindex="-1"></a> <span class="fu">discrete_scale</span>(<span class="at">aesthetics =</span> <span class="st">"fill"</span>,</span>
<span id="cb5-34"><a href="#cb5-34" aria-hidden="true" tabindex="-1"></a> <span class="at">scale_name =</span> <span class="st">"viridis modified"</span>,</span>
<span id="cb5-35"><a href="#cb5-35" aria-hidden="true" tabindex="-1"></a> <span class="at">palette =</span> colors) <span class="sc">+</span></span>
<span id="cb5-36"><a href="#cb5-36" aria-hidden="true" tabindex="-1"></a> <span class="fu">scale_y_continuous</span>(<span class="at">breaks=</span><span class="cf">function</span>(limits) <span class="fu">seq</span>(<span class="dv">0</span>, <span class="fu">floor</span>(limits[[<span class="dv">2</span>]]), <span class="at">by=</span><span class="dv">10</span>)) <span class="sc">+</span></span>
<span id="cb5-37"><a href="#cb5-37" aria-hidden="true" tabindex="-1"></a> <span class="fu">labs</span>(<span class="at">x=</span><span class="st">"seconds"</span>, <span class="at">y=</span><span class="st">"MB"</span>, <span class="at">fill =</span> <span class="st">"Cost center"</span>)</span></code></pre></div>
<figure>
<img src="/img/heap-profile-ggplot-stacked.svg"
alt="A stacked graph of the heap profile (produced with ggplot)" />
<figcaption aria-hidden="true">A stacked graph of the heap profile
(produced with ggplot)</figcaption>
</figure>
<p>But these stacked plots are not always the best way to represent the
data. Let’s see what happens if we try a simple line plot.</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a>top_entries <span class="sc">%>%</span></span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a> <span class="fu">ggplot</span>(<span class="fu">aes</span>(time,value,<span class="at">color=</span>name_sorted)) <span class="sc">+</span></span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a> <span class="fu">geom_line</span>() <span class="sc">+</span></span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a> <span class="fu">scale_y_continuous</span>(<span class="at">breaks=</span><span class="cf">function</span>(limits) <span class="fu">seq</span>(<span class="dv">0</span>, <span class="fu">floor</span>(limits[[<span class="dv">2</span>]]), <span class="at">by=</span><span class="dv">5</span>)) <span class="sc">+</span></span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a> <span class="fu">labs</span>(<span class="at">x=</span><span class="st">"seconds"</span>, <span class="at">y=</span><span class="st">"MB"</span>, <span class="at">color =</span> <span class="st">"Cost center"</span>)</span></code></pre></div>
<figure>
<img src="/img/heap-profile-ggplot-lines.svg"
alt="A line graph of the heap profile" />
<figcaption aria-hidden="true">A line graph of the heap
profile</figcaption>
</figure>
<p>This looks weird, doesn’t it? Do those lines merge, or does one of
them just disappear?</p>
<p>To disentangle this graph a bit, we can add a random offset for each
cost-center.</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="fu">set.seed</span>(<span class="dv">2020</span>)</span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>top_entries <span class="sc">%>%</span></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a> <span class="fu">group_by</span>(name) <span class="sc">%>%</span></span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a> <span class="fu">mutate</span>(<span class="at">value =</span> value <span class="sc">+</span> <span class="fu">runif</span>(<span class="dv">1</span>,<span class="dv">0</span>,<span class="dv">3</span>)) <span class="sc">%>%</span></span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a> ungroup <span class="sc">%>%</span></span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a> <span class="fu">ggplot</span>(<span class="fu">aes</span>(time,value,<span class="at">color=</span>name_sorted)) <span class="sc">+</span></span>
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a> <span class="fu">geom_line</span>() <span class="sc">+</span></span>
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a> <span class="fu">scale_y_continuous</span>(<span class="at">breaks=</span><span class="cf">function</span>(limits) <span class="fu">seq</span>(<span class="dv">0</span>, <span class="fu">floor</span>(limits[[<span class="dv">2</span>]]), <span class="at">by=</span><span class="dv">5</span>)) <span class="sc">+</span></span>
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a> <span class="fu">labs</span>(<span class="at">x=</span><span class="st">"seconds"</span>, <span class="at">y=</span><span class="st">"MB"</span>, <span class="at">color =</span> <span class="st">"Cost center"</span>)</span></code></pre></div>
<figure>
<img src="/img/heap-profile-ggplot-lines-random-offset.svg"
alt="A line graph of the heap profile, with a random offset added per cost center" />
<figcaption aria-hidden="true">A line graph of the heap profile, with a
random offset added per cost center</figcaption>
</figure>
<p>So it’s not a glitch, and indeed several cost centers have identical
dynamics. It’s not hard to imagine why this could happen: think about
tuples whose elements occupy the same amount of space but are produced
by different cost centers. As these tuples are consumed and
garbage-collected, the corresponding lines remain in perfect sync. But
this effect wasn’t obvious at all from the stacked plot, was it?</p>
<p>Another thing that is hard to understand from a stacked plot is how
different cost centers compare, say, in terms of their maximum resident
size. But in R, we can easily visualize this with a simple bar plot:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode r"><code class="sourceCode r"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a>top_entries <span class="ot"><-</span> csv <span class="sc">%>%</span></span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a> <span class="fu">group_by</span>(name) <span class="sc">%>%</span></span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a> <span class="fu">summarize</span>(<span class="at">max_value =</span> <span class="fu">max</span>(value)) <span class="sc">%>%</span></span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a> <span class="fu">filter</span>(max_value <span class="sc">>=</span> <span class="dv">1</span>) <span class="sc">%>%</span></span>
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a> <span class="fu">arrange</span>(max_value) <span class="sc">%>%</span></span>
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a> <span class="fu">mutate</span>(<span class="at">name =</span> <span class="fu">str_trunc</span>(name, <span class="dv">120</span>), <span class="at">name =</span> <span class="fu">factor</span>(name, <span class="at">levels=</span>name))</span>
<span id="cb8-7"><a href="#cb8-7" aria-hidden="true" tabindex="-1"></a><span class="fu">ggplot</span>(top_entries, <span class="fu">aes</span>(name,max_value)) <span class="sc">+</span> <span class="fu">geom_col</span>(<span class="at">fill=</span><span class="fu">viridis_pal</span>(<span class="at">alpha=</span><span class="fl">0.7</span>)(<span class="dv">5</span>)[[<span class="dv">4</span>]]) <span class="sc">+</span></span>
<span id="cb8-8"><a href="#cb8-8" aria-hidden="true" tabindex="-1"></a> <span class="fu">geom_text</span>(<span class="fu">aes</span>(name,<span class="at">label=</span>name),<span class="at">y=</span><span class="dv">0</span>,<span class="at">hjust=</span><span class="st">"left"</span>) <span class="sc">+</span></span>
<span id="cb8-9"><a href="#cb8-9" aria-hidden="true" tabindex="-1"></a> <span class="fu">labs</span>(<span class="at">x=</span><span class="st">"Cost center"</span>, <span class="at">y=</span><span class="st">"Memory, MB"</span>) <span class="sc">+</span></span>
<span id="cb8-10"><a href="#cb8-10" aria-hidden="true" tabindex="-1"></a> <span class="fu">scale_x_discrete</span>(<span class="at">breaks=</span><span class="cn">NULL</span>) <span class="sc">+</span></span>
<span id="cb8-11"><a href="#cb8-11" aria-hidden="true" tabindex="-1"></a> <span class="fu">scale_y_continuous</span>(<span class="at">breaks=</span><span class="cf">function</span>(limits) <span class="fu">seq</span>(<span class="dv">0</span>, <span class="fu">floor</span>(limits[[<span class="dv">2</span>]]), <span class="at">by=</span><span class="dv">5</span>)) <span class="sc">+</span></span>
<span id="cb8-12"><a href="#cb8-12" aria-hidden="true" tabindex="-1"></a> <span class="fu">coord_flip</span>()</span></code></pre></div>
<figure>
<img src="/img/heap-profile-ggplot-barplot.svg"
alt="A bar plot of the maximum residenct size per cost center" />
<figcaption aria-hidden="true">A bar plot of the maximum residenct size
per cost center</figcaption>
</figure>
<p>Finally, in R you are not limited to just visualization; you can do
all sorts of data analyses. For instance, a few years back I needed to
verify that, in a server process, a certain function was not consuming
increasingly more memory over time. I used this technique to load the
heap profile into R and verify that with more confidence that I would
have had from looking at a stacked graph.</p>
<h2 id="hp2any">hp2any</h2>
<p>One issue with big Haskell projects is that, if not actively
maintained, they tend to bitrot due to the changes in the compiler, the
Haskell dependencies or even the C dependencies.</p>
<p>One such example is Patai Gergely’s <a
href="https://github.com/cobbpg/hp2any">hp2any</a>. It no longer builds
with the current version of the <code>network</code> package because of
some API changes. But even when I tried to build it with the included
<code>stack.yaml</code> file, I got</p>
<pre><code>glib > Linking /tmp/stack214336/glib-0.13.6.0/.stack-work/dist/x86_64-linux-tinfo6/Cabal-2.2.0.1/setup/setup ...
glib > Configuring glib-0.13.6.0...
glib > build
glib > Preprocessing library for glib-0.13.6.0..
glib > setup: Error in C header file.
glib >
glib > /usr/include/glib-2.0/glib/gspawn.h:76: (column 22) [FATAL]
glib > >>> Syntax error!
glib > The symbol `__attribute__' does not fit here.
glib > </code></pre>
<p>I’m guessing (only guessing) that this issue is fixed in the latest
versions of the glib Haskell package, but we can’t benefit from that
when using an old <code>stack.yaml</code>. This also shows a flaw in
some people’s argument that if you put prospective upper bounds on your
Haskell dependencies, your projects will build forever.</p>
<p>(At this point, someone will surely mention nix and how it would’ve
helped here. It probably would, but as an owner of a 50GB /nix
directory, I’m not so enthusiastic about adding another 5GB there
consisting of old OpenGL and GTK libraries just to get a heap profile
visualizer.)</p>
Thu, 14 May 2020 20:00:00 +0000
https://ro-che.info/articles/2020-05-14-visualize-haskell-heap-profiles
https://ro-che.info//articles/2020-05-14-visualize-haskell-heap-profiles.html
-
Compile and link a Haskell package against a local C library
<p>Let’s say you want to build a Haskell package with a locally built
version of a C library for testing/debugging purposes. Doing this is
easy once you know the right option names, but finding this information
took me some time, so I’m recording it here for the reference.</p>
<p>Let’s say the headers of your local library are in
<code>/home/user/src/mylib/include</code> and the library files
(<code>*.so</code> or <code>*.a</code>) are in
<code>/home/user/src/mylib/lib</code>. Then you can put the following
into your <code>stack.yaml</code> (tested with stack v2.2.0;
instructions for cabal-install should be similar):</p>
<div class="sourceCode" id="cb1"><pre
class="sourceCode yaml"><code class="sourceCode yaml"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="fu">extra-include-dirs</span><span class="kw">:</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="kw">-</span><span class="at"> /home/user/src/mylib/include</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="fu">extra-lib-dirs</span><span class="kw">:</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="kw">-</span><span class="at"> /home/user/src/mylib/lib</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a><span class="fu">ghc-options</span><span class="kw">:</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">"$locals"</span><span class="kw">:</span><span class="at"> -optl=-Wl,-rpath,/home/user/src/mylib/lib</span></span></code></pre></div>
<p>Here <code>"$locals"</code> means <a
href="https://docs.haskellstack.org/en/stable/yaml_configuration/#ghc-options">“apply
the options to all local packages”</a>.</p>
Tue, 07 Apr 2020 20:00:00 +0000
https://ro-che.info/articles/2020-04-07-haskell-local-c-library
https://ro-che.info//articles/2020-04-07-haskell-local-c-library.html