<?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: Youssef Bouha</title>
    <description>The latest articles on DEV Community by Youssef Bouha (@youssef_bouha_417febe3701).</description>
    <link>https://dev.to/youssef_bouha_417febe3701</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%2F1856264%2F41b3628f-5914-4b36-9f4b-543ed7a871b5.jpg</url>
      <title>DEV Community: Youssef Bouha</title>
      <link>https://dev.to/youssef_bouha_417febe3701</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/youssef_bouha_417febe3701"/>
    <language>en</language>
    <item>
      <title>Comparing two lists of 1 million objects in 0.7 s in .NET, and why the result should be data, not a boolean</title>
      <dc:creator>Youssef Bouha</dc:creator>
      <pubDate>Mon, 26 Oct 2026 00:10:42 +0000</pubDate>
      <link>https://dev.to/youssef_bouha_417febe3701/comparing-two-lists-of-1-million-objects-in-07-s-in-net-and-why-the-result-should-be-data-not-a-hl7</link>
      <guid>https://dev.to/youssef_bouha_417febe3701/comparing-two-lists-of-1-million-objects-in-07-s-in-net-and-why-the-result-should-be-data-not-a-hl7</guid>
      <description>&lt;p&gt;Every release of a pricing or risk engine raises the same question: &lt;em&gt;does the new version produce the same numbers as the old one?&lt;/em&gt; The usual answer is a non-regression test (NRT): run yesterday's build and today's build on the same input, then compare millions of output rows.&lt;/p&gt;

&lt;p&gt;Most NRT harnesses I have seen answer that question with a boolean, or with a hash of the whole output. "Different" is not an answer you can act on. What you need is &lt;strong&gt;which&lt;/strong&gt; rows differ, on &lt;strong&gt;which&lt;/strong&gt; properties, with &lt;strong&gt;what&lt;/strong&gt; values, and which rows are missing on either side.&lt;/p&gt;

&lt;p&gt;That is what &lt;a href="https://github.com/ybouha/Xero.DataComparer" rel="noopener noreferrer"&gt;Xero.DataComparer&lt;/a&gt; does. It is a small, dependency-free .NET library (&lt;code&gt;net48&lt;/code&gt;, &lt;code&gt;netstandard2.0&lt;/code&gt;, &lt;code&gt;net8.0&lt;/code&gt;, &lt;code&gt;net10.0&lt;/code&gt;) that I extracted from tooling I built over the years on capital-markets and energy-trading platforms. This post walks through the API and the few design choices that make it fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API in ten lines
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Xero.DataComparer.Core&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;comparer&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;ListComparer&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RiskIndicator&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;keyProperties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"TradeId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Book"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"AsOfDate"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;// how rows are matched&lt;/span&gt;
    &lt;span class="n"&gt;ignoreProperties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"LastUpdated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"RunId"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;         &lt;span class="c1"&gt;// volatile fields&lt;/span&gt;

&lt;span class="n"&gt;CompareResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RiskIndicator&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;comparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CompareListAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Only in reference : &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnlyInReference&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="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Only in target    : &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnlyInTarget&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="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"In both, differing: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&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="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You give it the key properties that identify the same row on both sides, the properties to ignore, and two &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt;. Every other public property of &lt;code&gt;T&lt;/code&gt; is compared.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get back
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CompareResult&amp;lt;T&amp;gt;&lt;/code&gt; holds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Slice&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;OnlyInReference&lt;/code&gt; / &lt;code&gt;OnlyInTarget&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The original objects present on one side only.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;DuplicatesInReference&lt;/code&gt; / &lt;code&gt;DuplicatesInTarget&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Items whose key was already seen on the same side.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the result itself&lt;/td&gt;
&lt;td&gt;One flat row per matched pair that differs on at least one property.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Timings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start and end of each step, for your own instrumentation.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A diff row is a small dictionary laid out for humans and tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Key_TradeId = TRD-000980002   Key_Book = FX-G10   Key_AsOfDate = 2026-04-30
Reference_PnL = 44,362.94     Target_PnL = 44,352.16
Reference_VaR = 49,823.45     Target_VaR = 49,860.02
ComparedItems = (reference object, target object)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the properties that actually differ are stored, and &lt;code&gt;ComparedItems&lt;/code&gt; keeps both original objects so you can navigate to any other field.&lt;/p&gt;

&lt;p&gt;The best part: &lt;code&gt;CompareResult&amp;lt;T&amp;gt;&lt;/code&gt; implements &lt;code&gt;ITypedList&lt;/code&gt;. Its columns are derived from &lt;code&gt;T&lt;/code&gt; (every key, every compared property, typed), so you can assign it to a WinForms &lt;code&gt;DataGridView&lt;/code&gt;, a DevExpress &lt;code&gt;GridControl&lt;/code&gt; or any data-bound grid, and the columns appear with no view model and no column setup. The repository includes a WinForms demo built exactly on that:&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%2Fqlg7wzitxoklfeai1avp.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%2Fqlg7wzitxoklfeai1avp.png" alt="Xero.DataComparer WinForms demo" width="800" height="494"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The silent bug: duplicate keys
&lt;/h2&gt;

&lt;p&gt;One thing an NRT must never do is lose rows silently. If two rows share the same key on the same side, a naive dictionary-based comparison keeps one and drops the other, and the report says "identical". In a regulatory or P&amp;amp;L context, that is the worst possible outcome.&lt;/p&gt;

&lt;p&gt;Xero.DataComparer returns those rows in &lt;code&gt;DuplicatesInReference&lt;/code&gt; / &lt;code&gt;DuplicatesInTarget&lt;/code&gt; and logs a warning. They are left out of the matching (there is no correct way to pair them), but they are never lost.&lt;/p&gt;

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

&lt;p&gt;On a 16-logical-core machine, comparing two lists of &lt;strong&gt;1,000,000 objects&lt;/strong&gt; takes about &lt;strong&gt;0.7 s&lt;/strong&gt; and allocates about &lt;strong&gt;53 MB&lt;/strong&gt;. Two lists of 5,000,000 take about 4.3 s. Four choices get it there.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Reflection once, typed delegates per row
&lt;/h3&gt;

&lt;p&gt;Property access is compiled once with &lt;code&gt;System.Linq.Expressions&lt;/code&gt;. The important detail is that the delegates are &lt;strong&gt;typed&lt;/strong&gt;: equality and hash code use &lt;code&gt;EqualityComparer&amp;lt;TProperty&amp;gt;.Default&lt;/code&gt; on the real property type, so comparing two &lt;code&gt;double&lt;/code&gt; values never boxes them.&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="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;PropertyAccessor&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CreateTyped&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;PropertyInfo&lt;/span&gt; &lt;span class="n"&gt;property&lt;/span&gt;&lt;span class="p"&gt;)&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;item&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"item"&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;getter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lambda&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;property&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Compile&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;comparer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EqualityComparer&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TProperty&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PropertyAccessor&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="n"&gt;property&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;                                          &lt;span class="c1"&gt;// boxes: only used for a diff row&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;comparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;getter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;         &lt;span class="c1"&gt;// no boxing&lt;/span&gt;
        &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;comparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetHashCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CreateTyped&amp;lt;TProperty&amp;gt;&lt;/code&gt; is called once per property through &lt;code&gt;MakeGenericMethod&lt;/code&gt;. After that, values are boxed only when a property actually differs, to be stored in the diff row.&lt;/p&gt;

&lt;p&gt;Switching from &lt;code&gt;Func&amp;lt;T, object&amp;gt;&lt;/code&gt; getters to typed accessors took the 1M-row comparison from &lt;strong&gt;1.4 s and 603 MB allocated to 0.75 s and 53 MB&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A dual hash set, sized up front, hash computed once
&lt;/h3&gt;

&lt;p&gt;Both lists go into a purpose-built &lt;code&gt;HashSetComparer&amp;lt;T&amp;gt;&lt;/code&gt;: two hash indexes sharing the same key comparer, with prime-sized buckets allocated from the known list sizes, so there is never a resize. Each item's hash code is stored when it is added and reused to look it up in the other index; it is never recomputed.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Everything that can run in parallel does
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;AddAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;referenceItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddLeftItem&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;AddAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targetItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddRightItem&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

&lt;span class="n"&gt;Parallel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;onlyInTarget&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnlyInRight&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;onlyInReference&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnlyInLeft&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;inBothButDiff&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;CompareInBoth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InBoth&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;   &lt;span class="c1"&gt;// itself PLINQ&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two indexes are built at the same time, then the three result sets are produced at the same time, and the property comparison of matched pairs is spread over all cores with PLINQ. &lt;code&gt;result.Timings&lt;/code&gt; shows how much the steps overlap; in the screenshot above, 1,559 ms of work runs in 676 ms of wall clock.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Tiny diff rows
&lt;/h3&gt;

&lt;p&gt;A diff row holds at most &lt;code&gt;keys + 2 × compared properties + 1&lt;/code&gt; entries, typically 10 to 30. At that size a linear scan beats hashing, so rows are a small &lt;code&gt;PooledDictionary&lt;/code&gt; sized exactly to the column count, and the column names (&lt;code&gt;Reference_PnL&lt;/code&gt;, ...) are built once per comparer, not once per row.&lt;/p&gt;

&lt;h2&gt;
  
  
  A benchmarking trap worth knowing
&lt;/h2&gt;

&lt;p&gt;While measuring this, my first numbers were terrible: 7.9 s and 7.4 GB for 1M rows. The cause had nothing to do with the library. File-based apps (&lt;code&gt;dotnet run app.cs&lt;/code&gt;) default to &lt;code&gt;PublishAot=true&lt;/code&gt;, and under that setting &lt;code&gt;Expression.Compile()&lt;/code&gt; falls back to the &lt;strong&gt;interpreter&lt;/strong&gt;. Every "compiled" getter call was interpreted and allocated. Adding &lt;code&gt;#:property PublishAot=false&lt;/code&gt; at the top of the benchmark file fixed it. If you use expression trees in a file-based app, check this first.&lt;/p&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;dotnet add package Xero.DataComparer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Source, console demo (10M rows), WinForms demo and tests: &lt;a href="https://github.com/ybouha/Xero.DataComparer" rel="noopener noreferrer"&gt;github.com/ybouha/Xero.DataComparer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;NuGet: &lt;a href="https://www.nuget.org/packages/Xero.DataComparer" rel="noopener noreferrer"&gt;nuget.org/packages/Xero.DataComparer&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback, issues and benchmarks on your own data are very welcome.&lt;/p&gt;

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