<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ddalton</title>
    <description>The latest articles on DEV Community by ddalton (@ddalton).</description>
    <link>https://dev.to/ddalton</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4169950%2F469194fe-230f-4125-946a-1787810b965e.png</url>
      <title>DEV Community: ddalton</title>
      <link>https://dev.to/ddalton</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ddalton"/>
    <language>en</language>
    <item>
      <title>Is your cache the right size? fliplru can tell you</title>
      <dc:creator>ddalton</dc:creator>
      <pubDate>Thu, 08 Oct 2026 00:02:43 +0000</pubDate>
      <link>https://dev.to/ddalton/is-your-cache-the-right-size-fliplru-can-tell-you-58im</link>
      <guid>https://dev.to/ddalton/is-your-cache-the-right-size-fliplru-can-tell-you-58im</guid>
      <description>&lt;p&gt;Every cache has a capacity, and almost every capacity is a guess. Too small and the cache&lt;br&gt;
thrashes: entries are evicted just before they are needed again. Too large and you pay&lt;br&gt;
memory for nothing. Most LRU caches give you no way to tell which one you have.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://crates.io/crates/fliplru" rel="noopener noreferrer"&gt;fliplru&lt;/a&gt; is a small, fast LRU cache for Rust, &lt;code&gt;no_std&lt;/code&gt;&lt;br&gt;
and safe, that measures its own fit and turns that measurement into a verdict:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;fliplru&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;LruCache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Sizing&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;num&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NonZeroUsize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;LruCache&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;NonZeroUsize&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="c1"&gt;// ... run your workload ...&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="nf"&gt;.stats&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hit ratio {:.1}%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="nf"&gt;.hit_ratio&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="nf"&gt;.sizing&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;Sizing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Oversized&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;needed&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"only {needed} entries were ever needed"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nn"&gt;Sizing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;TooSmall&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"up to 2x the capacity would turn lucky hits into reliable ones"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nn"&gt;Sizing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;MuchTooSmall&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"several times the capacity would help a lot"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nn"&gt;Sizing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Thrashing&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"almost nothing is reused before it is evicted"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nn"&gt;Sizing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Fits&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"a bigger cache would gain little"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"run longer"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This post covers how that works, how the verdicts were checked, and how fliplru compares&lt;br&gt;
with the popular LRU crates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two generations and a flip
&lt;/h2&gt;

&lt;p&gt;fliplru keeps two hash maps, a &lt;em&gt;current&lt;/em&gt; generation and a &lt;em&gt;previous&lt;/em&gt; one. New and recently&lt;br&gt;
used keys go into the current one. When it holds &lt;code&gt;cap&lt;/code&gt; entries the cache &lt;strong&gt;flips&lt;/strong&gt;: the&lt;br&gt;
current generation becomes the previous one, the old previous generation is dropped, and an&lt;br&gt;
empty current generation begins. A lookup that finds its key in the previous generation&lt;br&gt;
moves it back into the current one, so anything still in use survives the next flip.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm6li5lgvcu13an7kvjq0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm6li5lgvcu13an7kvjq0.png" alt="How a flip works: the full current generation becomes the previous one, the old previous one is dropped, and a key found in the previous generation moves back" width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The design is old (the JavaScript &lt;code&gt;hashlru&lt;/code&gt; package works the same way), and it has three&lt;br&gt;
useful properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;get&lt;/code&gt; of a recently used key is a single hash-table lookup, with no linked list to
update.&lt;/li&gt;
&lt;li&gt;The last &lt;code&gt;cap&lt;/code&gt; distinct keys you used are always present. Up to &lt;code&gt;2 * cap&lt;/code&gt; may be, since
the previous generation holds on to more, so you plan memory for &lt;code&gt;2 * cap&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flips measure turnover.&lt;/strong&gt; No flips means everything you use fits. Flips approaching
&lt;code&gt;accesses / cap&lt;/code&gt; means almost nothing is used twice before it is dropped.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  From a flip count to a verdict
&lt;/h2&gt;

&lt;p&gt;The flip count alone already tells you a lot, and &lt;code&gt;stats()&lt;/code&gt; adds the numbers around it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;counter&lt;/th&gt;
&lt;th&gt;what it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hits&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;lookups found in the current generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;promotions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;lookups found in the previous generation: hits, but lucky ones, since that generation was about to be dropped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;misses&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;lookups that found nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;inserts&lt;/code&gt;, &lt;code&gt;updates&lt;/code&gt;, &lt;code&gt;flips&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;peak_entries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the most entries ever held: the memory the workload actually needed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Promotions are the interesting one.&lt;/strong&gt; A promotion means the key was reused at a distance&lt;br&gt;
between &lt;code&gt;cap&lt;/code&gt; and &lt;code&gt;2 * cap&lt;/code&gt;. Many promotions mean the keys in use slightly outnumber the&lt;br&gt;
capacity, and a modest increase would turn those lucky hits into reliable ones. No&lt;br&gt;
conventional LRU can report this, because it has no second generation to catch the near&lt;br&gt;
misses.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F10wvi71xy2dek3jl0oxj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F10wvi71xy2dek3jl0oxj.png" alt="One lookup: a hit in the current generation, a promotion from the previous one, or a miss, and the counter each one bumps" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;stats().sizing()&lt;/code&gt; combines these into one of six verdicts. The rules are simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;NotEnoughData&lt;/code&gt;&lt;/strong&gt;: fewer than 10 x cap lookups so far.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Oversized { needed }&lt;/code&gt;&lt;/strong&gt;: there were no flips. &lt;code&gt;needed&lt;/code&gt; is the peak number of entries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Thrashing&lt;/code&gt;&lt;/strong&gt;: the hit ratio is under 10%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;TooSmall&lt;/code&gt;&lt;/strong&gt;: more than a quarter of hits were promotions, and either the hit ratio is
at least 50% or nearly all hits were promotions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;MuchTooSmall&lt;/code&gt;&lt;/strong&gt;: more than a quarter of hits were promotions, with a low hit ratio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Fits&lt;/code&gt;&lt;/strong&gt;: anything else.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Checking the verdicts against the truth
&lt;/h3&gt;

&lt;p&gt;Rules like these are easy to write and easy to get wrong, so I checked them against ground&lt;br&gt;
truth. I ran each test workload at the chosen capacity, then again at half, double and four&lt;br&gt;
times that capacity. If halving costs nothing, the cache was oversized. If doubling gains a&lt;br&gt;
lot, it was too small. If doubling gains little, it fits.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgd33g18s3pumyesiz4hc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgd33g18s3pumyesiz4hc.png" alt="Hit ratio at half, one, two and four times the capacity for one workload per verdict" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The test set was 21 scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fixed working sets of 0.3 to 20 times the capacity, in rotation and at random;&lt;/li&gt;
&lt;li&gt;Zipf traffic (a few very popular keys and a long tail) at skews 0.7 to 1.2;&lt;/li&gt;
&lt;li&gt;Zipf traffic interrupted by one-off scans.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each ran at capacities of 1,000, 10,000 and 100,000. A few of the results:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;workload (cap 10,000)&lt;/th&gt;
&lt;th&gt;hit ratio&lt;/th&gt;
&lt;th&gt;at 2x cap&lt;/th&gt;
&lt;th&gt;at 4x cap&lt;/th&gt;
&lt;th&gt;verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8,000 keys in rotation&lt;/td&gt;
&lt;td&gt;99.2%&lt;/td&gt;
&lt;td&gt;99.2%&lt;/td&gt;
&lt;td&gt;99.2%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Oversized { needed: 8000 }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12,000 keys in rotation&lt;/td&gt;
&lt;td&gt;79.2%&lt;/td&gt;
&lt;td&gt;98.8%&lt;/td&gt;
&lt;td&gt;98.8%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TooSmall&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50,000 keys at random&lt;/td&gt;
&lt;td&gt;28.0%&lt;/td&gt;
&lt;td&gt;52.1%&lt;/td&gt;
&lt;td&gt;86.7%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MuchTooSmall&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200,000 keys at random&lt;/td&gt;
&lt;td&gt;7.4%&lt;/td&gt;
&lt;td&gt;14.4%&lt;/td&gt;
&lt;td&gt;27.5%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Thrashing&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zipf 0.99 over 100,000 keys&lt;/td&gt;
&lt;td&gt;75.4%&lt;/td&gt;
&lt;td&gt;82.6%&lt;/td&gt;
&lt;td&gt;89.0%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Fits&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the same, with scans&lt;/td&gt;
&lt;td&gt;18.5%&lt;/td&gt;
&lt;td&gt;21.7%&lt;/td&gt;
&lt;td&gt;23.7%&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Fits&lt;/code&gt; (scans cannot be cached)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first version of the rules called the 50,000-key case &lt;code&gt;TooSmall&lt;/code&gt; ("raise it a&lt;br&gt;
little"), when it really needed about four times the capacity. That is why there are two&lt;br&gt;
"too small" verdicts. With the split, all 21 scenarios get the right verdict at all three&lt;br&gt;
capacities. One case per verdict is now a test in the crate.&lt;/p&gt;

&lt;p&gt;The counters are plain integers updated as the cache runs. They cost nothing measurable on&lt;br&gt;
lookups and about 0.4 ns per &lt;code&gt;put&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How fast is it?
&lt;/h2&gt;

&lt;p&gt;Here is fliplru against the most-used LRU crates on an Apple M1, with a capacity of 100,000 and times in nanoseconds per operation:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;cache&lt;/th&gt;
&lt;th&gt;get (all hits)&lt;/th&gt;
&lt;th&gt;put&lt;/th&gt;
&lt;th&gt;Zipf, integer keys&lt;/th&gt;
&lt;th&gt;loop, String keys&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;fliplru&lt;/td&gt;
&lt;td&gt;7.1&lt;/td&gt;
&lt;td&gt;16.3&lt;/td&gt;
&lt;td&gt;15.4&lt;/td&gt;
&lt;td&gt;84.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fliplru + Fx hasher&lt;/td&gt;
&lt;td&gt;5.3&lt;/td&gt;
&lt;td&gt;6.0&lt;/td&gt;
&lt;td&gt;14.2&lt;/td&gt;
&lt;td&gt;85.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lru 0.18&lt;/td&gt;
&lt;td&gt;8.7&lt;/td&gt;
&lt;td&gt;36.5&lt;/td&gt;
&lt;td&gt;16.7&lt;/td&gt;
&lt;td&gt;106.4 (every request misses)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;schnellru 0.2&lt;/td&gt;
&lt;td&gt;7.3&lt;/td&gt;
&lt;td&gt;25.5&lt;/td&gt;
&lt;td&gt;14.8&lt;/td&gt;
&lt;td&gt;100.8 (every request misses)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;quick_cache 0.7&lt;/td&gt;
&lt;td&gt;7.7&lt;/td&gt;
&lt;td&gt;31.2&lt;/td&gt;
&lt;td&gt;19.9&lt;/td&gt;
&lt;td&gt;74.9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read-throughs use each crate's own best "get or insert" method. fliplru was measured&lt;br&gt;
before the counters were added; they add about 0.4 ns to each &lt;code&gt;put&lt;/code&gt;. fliplru is clearly&lt;br&gt;
fastest on puts, and on gets with a faster hasher. On realistic read traffic it is level with the&lt;br&gt;
best; with String keys, the top three trade places from run to run.&lt;/p&gt;

&lt;p&gt;That is also where I will be careful. &lt;strong&gt;Speed is not the main reason to choose fliplru.&lt;/strong&gt;&lt;br&gt;
When a miss costs a database query, a few points of hit ratio matter far more than a few&lt;br&gt;
nanoseconds per hit. And at equal memory, fliplru's hit ratio is lower than a classic LRU's&lt;br&gt;
(78.7% against 82.0% on Zipf traffic), because a flip drops a whole generation at once&lt;br&gt;
where an LRU drops one key at a time. If hit ratio is what you need,&lt;br&gt;
&lt;a href="https://crates.io/crates/quick_cache" rel="noopener noreferrer"&gt;quick_cache&lt;/a&gt; (S3-FIFO) is better, especially on&lt;br&gt;
traffic with one-off scans.&lt;/p&gt;

&lt;p&gt;So fliplru fits best where operations are very frequent and misses are cheap (memoizing&lt;br&gt;
small computations, interning, write-heavy caches), or wherever you want the cache to tell&lt;br&gt;
you whether it is the right size.&lt;/p&gt;

&lt;p&gt;It is also &lt;code&gt;no_std&lt;/code&gt; and builds for bare-metal targets such as Cortex-M (it needs an&lt;br&gt;
allocator). All of its memory is allocated when the cache is created, and flips reuse it, so&lt;br&gt;
it never allocates afterwards, which matters where heap fragmentation is a risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design notes
&lt;/h2&gt;

&lt;p&gt;Most of the speed comes from ordinary care: the two generations are hashbrown &lt;code&gt;HashTable&lt;/code&gt;s&lt;br&gt;
sharing one hasher, so a key is hashed once per operation; a promotion takes two table&lt;br&gt;
operations; a flip reuses the retired table's memory; and &lt;code&gt;get_or_insert_with&lt;/code&gt; does a&lt;br&gt;
read-through with a single hash.&lt;/p&gt;

&lt;p&gt;The fast path has one Rust-specific wrinkle. "Return the value if it is in the current&lt;br&gt;
generation, otherwise look in the previous one" is a known limitation of today's borrow&lt;br&gt;
checker: returning a borrow from one branch, then using the map again in the other.&lt;br&gt;
&lt;a href="https://crates.io/crates/polonius-the-crab" rel="noopener noreferrer"&gt;polonius-the-crab&lt;/a&gt; makes that single-lookup&lt;br&gt;
version expressible in safe Rust, with no runtime cost.&lt;/p&gt;

&lt;p&gt;Two experiments did not pay off:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One table instead of two,&lt;/strong&gt; with each entry tagged by its generation. Promotions became
a single in-place write, and gets got 15% faster. But dropping a generation from a shared
table leaves deleted markers that lookups have to skip, and puts became &lt;strong&gt;2.2x slower&lt;/strong&gt;.
The second table's probe that this was meant to save turned out to be cheap: hashbrown
checks a whole group of slots in one SIMD comparison.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A "probation" generation,&lt;/strong&gt; the admission idea behind S3-FIFO: new keys must prove
themselves before entering the main cache. It nearly matched S3-FIFO on scans, but it
gained little on ordinary traffic. It also broke fliplru's guarantee that the last &lt;code&gt;cap&lt;/code&gt;
keys are always present, which is the property the sizing signal relies on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Benchmarking lessons
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run order matters.&lt;/strong&gt; On a laptop, the order in which benchmarks run moved results by
10-20%. The harness in the repo runs every cache in every round and rotates the order
between rounds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report hit ratio next to time.&lt;/strong&gt; A cache that misses more is doing different work. On a
loop slightly larger than the capacity, a classic LRU misses every request, and its time
there measures misses, not hits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use each crate's best API.&lt;/strong&gt; Comparing your &lt;code&gt;get_or_insert&lt;/code&gt; against someone else's
&lt;code&gt;get&lt;/code&gt; followed by &lt;code&gt;put&lt;/code&gt; is not a fair comparison.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo add fliplru
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The docs are on &lt;a href="https://docs.rs/fliplru" rel="noopener noreferrer"&gt;docs.rs&lt;/a&gt;, and the benchmark harness is in the&lt;br&gt;
&lt;a href="https://github.com/ddalton/fliplru" rel="noopener noreferrer"&gt;repository&lt;/a&gt;: &lt;code&gt;cd bench &amp;amp;&amp;amp; cargo run --release&lt;/code&gt;.&lt;br&gt;
If &lt;code&gt;stats().sizing()&lt;/code&gt; tells you something surprising about one of your caches, I would like&lt;br&gt;
to hear about it.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>performance</category>
      <category>caching</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
