<?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: ReArch Labs</title>
    <description>The latest articles on DEV Community by ReArch Labs (rearchlabs).</description>
    <link>https://dev.to/rearchlabs</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%2Forganization%2Fprofile_image%2F15397%2Fe8744a03-c08a-4493-9c98-8b110efdcc16.png</url>
      <title>DEV Community: ReArch Labs</title>
      <link>https://dev.to/rearchlabs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rearchlabs"/>
    <language>en</language>
    <item>
      <title>Replacing repeated List.Find calls in a .NET catalog import: measure the index build too</title>
      <dc:creator>Mert C Balcı</dc:creator>
      <pubDate>Sun, 18 Oct 2026 19:39:04 +0000</pubDate>
      <link>https://dev.to/rearchlabs/replacing-repeated-listfind-calls-in-a-net-catalog-import-measure-the-index-build-too-18lc</link>
      <guid>https://dev.to/rearchlabs/replacing-repeated-listfind-calls-in-a-net-catalog-import-measure-the-index-build-too-18lc</guid>
      <description>&lt;p&gt;A catalog import often has a simple contract: for every incoming SKU, find its product, multiply the price by the quantity, and preserve the input order. A small lookup inside that loop can dominate the work when it scans the same catalog repeatedly.&lt;/p&gt;

&lt;p&gt;This article adapts the &lt;a href="https://rearchlabs.com/en/lab/dotnet-catalog-import" rel="noopener noreferrer"&gt;ReArch Labs catalog-import experiment published on 15 October 2026&lt;/a&gt;. It explains the existing source and recorded results; no new benchmark was run for this article. The workload is synthetic, not a customer migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the result that must stay the same
&lt;/h2&gt;

&lt;p&gt;The fixture contains 10,000 unique product SKUs and import batches of 1,000, 10,000 and 50,000 rows. Every row where &lt;code&gt;i % 17 == 0&lt;/code&gt; has an unknown SKU. A matched row produces &lt;code&gt;PriceCents * Quantity&lt;/code&gt;; an unknown SKU produces &lt;code&gt;-1&lt;/code&gt;. Results retain the row order. Prices use integer cents, and SKU comparisons are ordinal and case-sensitive.&lt;/p&gt;

&lt;p&gt;These details matter before changing the collection. An index that silently changes matching rules or output order has changed the job, even if its stopwatch result looks better.&lt;/p&gt;

&lt;p&gt;The original loop uses &lt;code&gt;List.Find&lt;/code&gt;. This excerpt preserves the implementation; the surrounding harness is omitted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ImportRow&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sku&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sku&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PriceCents&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&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;&lt;code&gt;Find&lt;/code&gt; walks the list until it reaches a match. Repeating that search for every row repeats the catalog traversal; the predicate in this implementation also captures &lt;code&gt;row&lt;/code&gt;. Microsoft documents &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.find?view=net-10.0" rel="noopener noreferrer"&gt;&lt;code&gt;List.Find&lt;/code&gt; as a linear search&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The dictionary arm builds an index once per batch and then performs key lookups. These are corresponding excerpts from the same source, with formatting condensed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dictionary&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ordinal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;catalog&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;dictionary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ImportRow&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dictionary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PriceCents&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&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 changes both the lookup structure and the capturing-predicate allocation pattern. The result does not isolate the cost of either change. The fixture also has unique catalog keys; it does not establish a duplicate-SKU policy for a real importer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put index construction inside the stopwatch
&lt;/h2&gt;

&lt;p&gt;A cached dictionary comparison would answer a different question. Here, every batch builds a new dictionary or frozen dictionary. The timer includes that construction, the row matching and the output array allocation. The published harness separately records index construction time, but does not subtract it from the total.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement choice&lt;/th&gt;
&lt;th&gt;Recorded setup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Implementations&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;List.Find&lt;/code&gt;, &lt;code&gt;Dictionary&lt;/code&gt;, &lt;code&gt;FrozenDictionary&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;The same .NET 10.0.13 runtime for all three&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repetitions&lt;/td&gt;
&lt;td&gt;2 warmups and 10 measured runs per implementation and size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution order&lt;/td&gt;
&lt;td&gt;Fixed seeded shuffle in one process; 18 warmups and 90 measured runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environment&lt;/td&gt;
&lt;td&gt;Shared Apple M3 development machine; Docker Linux arm64; 1 CPU quota; 768 MiB memory limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JIT setting&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DOTNET_TieredCompilation=0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outside the timer&lt;/td&gt;
&lt;td&gt;Input generation, explicit pre-run full GC and output hashing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outside the workload&lt;/td&gt;
&lt;td&gt;CSV parsing, database writes and network calls&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Elapsed time comes from &lt;code&gt;Stopwatch&lt;/code&gt;. Process CPU time uses Linux &lt;code&gt;CLOCK_PROCESS_CPUTIME_ID&lt;/code&gt;. Managed allocation is the difference between two &lt;code&gt;GC.GetTotalAllocatedBytes(true)&lt;/code&gt; readings; it is not peak memory or RSS.&lt;/p&gt;

&lt;p&gt;All 108 outputs, including warmups, are checked against a separate Python reference implementation. The checks cover output count, missing-SKU count, sum in cents and SHA-256 of the ordered signed little-endian 64-bit results. “Separate reference” describes the checker's implementation, not an independent third-party audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the small batches as well as the headline
&lt;/h2&gt;

&lt;p&gt;These are elapsed-time medians in milliseconds from the &lt;a href="https://rearchlabs.com/labs/modernization-20261015/dotnet/full/raw.csv" rel="noopener noreferrer"&gt;published raw CSV&lt;/a&gt;, excluding warmups. Each cell contains ten measurements; values are rounded to three decimal places.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Import rows&lt;/th&gt;
&lt;th&gt;List.Find&lt;/th&gt;
&lt;th&gt;Dictionary&lt;/th&gt;
&lt;th&gt;FrozenDictionary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;14.216&lt;/td&gt;
&lt;td&gt;0.210&lt;/td&gt;
&lt;td&gt;0.897&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;143.355&lt;/td&gt;
&lt;td&gt;0.466&lt;/td&gt;
&lt;td&gt;1.059&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50,000&lt;/td&gt;
&lt;td&gt;708.219&lt;/td&gt;
&lt;td&gt;1.506&lt;/td&gt;
&lt;td&gt;1.961&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The prespecified primary point was 50,000 rows: about 708.22 ms for the original implementation and 1.51 ms for the dictionary implementation, including construction. That is an observation about this fixture and environment, not a runtime-upgrade speed claim. &lt;code&gt;Dictionary&lt;/code&gt; is available on older .NET versions too.&lt;/p&gt;

&lt;p&gt;The frozen collection is useful as a control because a newer API does not settle the choice. At 50,000 rows, median construction was about 0.183 ms for &lt;code&gt;Dictionary&lt;/code&gt; and 0.923 ms for &lt;code&gt;FrozenDictionary&lt;/code&gt;. For this rebuild-per-batch workload, the latter's total median was higher. A long-lived catalog reused across many batches would have a different cost balance; this experiment did not measure it. Microsoft's &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.collections.frozen.frozendictionary-2?view=net-10.0" rel="noopener noreferrer"&gt;&lt;code&gt;FrozenDictionary&lt;/code&gt; documentation&lt;/a&gt; discusses the tradeoff between construction cost and repeated reads.&lt;/p&gt;

&lt;p&gt;Small batches also expose the fixed allocation cost: an index can allocate more managed memory than the original list loop at the smallest size. The &lt;a href="https://rearchlabs.com/labs/modernization-20261015/dotnet/full/summary.json" rel="noopener noreferrer"&gt;full summary&lt;/a&gt; retains those results and the observed minimum/maximum timings. A median alone does not describe latency variation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the experiment, then change one question
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/MCB803/rearch-java-runtime-lab/tree/main/modernization-20261015/dotnet/full/sources" rel="noopener noreferrer"&gt;published source directory and README&lt;/a&gt; include the C# program, Python harness and tests. From that directory, with Python 3 and Docker available, the documented procedure is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; unittest discover &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'test_harness.py'&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt;
docker pull &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/arm64 mcr.microsoft.com/dotnet/sdk:10.0
python3 run.py &lt;span class="nt"&gt;--output&lt;/span&gt; /tmp/rearch-dotnet-catalog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tag can move. To target the recorded image, use the exact digest from the &lt;a href="https://rearchlabs.com/labs/modernization-20261015/dotnet/full/manifest.json" rel="noopener noreferrer"&gt;published manifest&lt;/a&gt; for the pull and the harness's &lt;code&gt;--image&lt;/code&gt; option. Choose a new output directory; the harness does not reuse an existing one. These are reproduction instructions, not a claim that they were executed while preparing this article.&lt;/p&gt;

&lt;p&gt;Before applying the change to an application, define duplicate-key handling, case sensitivity and missing-product behavior. Verify the same ordered outputs, then measure with the application's actual catalog size, miss rate and index lifetime. If the user-visible wait includes file parsing or database writes, measure that full path too.&lt;/p&gt;

&lt;p&gt;The recorded runs share one process and a development host; they are not independent-process trials on an isolated production machine. Disabled tiered compilation and pre-run GC also differ from many application settings. The experiment establishes a bounded result for an in-memory matching job. It does not measure production throughput, security, cloud cost or the benefit of upgrading .NET itself.&lt;/p&gt;

&lt;p&gt;Prepared with AI tools; source code and measurement data are linked above.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>performance</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
