Skip to content

Commit e67deb2

Browse files
committed
OrderBook: stop snapshotting the whole book on every consume
ConsumeVolume copied the entire quotes dictionary with quotes.ToArray() on every match, just so emptied price levels could be removed while iterating. That is a per-match allocation proportional to book depth on a hot path. Iterate the live dictionary instead and collect the emptied prices, removing them in a finally block. The dictionary is never mutated during its own enumeration, and because disposing the iterator runs the finally, the removal still happens even though the matcher abandons the enumerator early (it breaks out as soon as the order is filled) - which is the reason the snapshot was introduced in the first place. The only remaining allocation is the affected-orders list that is returned to the caller, and the small removal list, allocated lazily only when a level is actually emptied. Behaviour is unchanged: 203 matching-engine and market-emulator tests pass.
1 parent 41df256 commit e67deb2

1 file changed

Lines changed: 35 additions & 18 deletions

File tree

MatchingEngine/OrderBook.cs

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -376,28 +376,37 @@ public bool TryRemoveOrder(long transactionId, Sides side, out EmulatorOrder ord
376376
var quotes = GetQuotes(side);
377377
var remaining = volume;
378378

379-
foreach (var kvp in quotes.ToArray())
380-
{
381-
if (remaining <= 0)
382-
break;
383-
384-
var price = kvp.Key;
379+
// Levels emptied while consuming are collected and removed in the finally below. Doing it there (instead of
380+
// snapshotting the whole book with quotes.ToArray() on every match) keeps the dictionary unmodified during
381+
// its own enumeration, yet the removal still runs when the caller abandons the enumerator early - it breaks
382+
// out of the loop as soon as the order is filled, and disposing the iterator runs the finally.
383+
List<decimal> toRemove = null;
385384

386-
// Check price limit
387-
if (maxPrice.HasValue)
385+
try
386+
{
387+
foreach (var kvp in quotes)
388388
{
389-
if (side == Sides.Sell && price > maxPrice.Value)
389+
if (remaining <= 0)
390390
break;
391-
if (side == Sides.Buy && price < maxPrice.Value)
392-
break;
393-
}
394391

395-
var level = kvp.Value;
396-
var available = level.TotalVolume;
397-
var consumed = remaining.Min(available);
392+
var price = kvp.Key;
393+
394+
// Check price limit
395+
if (maxPrice.HasValue)
396+
{
397+
if (side == Sides.Sell && price > maxPrice.Value)
398+
break;
399+
if (side == Sides.Buy && price < maxPrice.Value)
400+
break;
401+
}
402+
403+
var level = kvp.Value;
404+
var available = level.TotalVolume;
405+
var consumed = remaining.Min(available);
406+
407+
if (consumed <= 0)
408+
continue;
398409

399-
if (consumed > 0)
400-
{
401410
var affectedOrders = level.Orders.ToList();
402411

403412
// Reduce market volume first
@@ -423,10 +432,18 @@ public bool TryRemoveOrder(long transactionId, Sides side, out EmulatorOrder ord
423432
remaining -= consumed;
424433

425434
if (level.IsEmpty)
426-
quotes.Remove(price);
435+
(toRemove ??= []).Add(price);
427436

428437
yield return (price, consumed, affectedOrders);
429438
}
430439
}
440+
finally
441+
{
442+
if (toRemove != null)
443+
{
444+
foreach (var price in toRemove)
445+
quotes.Remove(price);
446+
}
447+
}
431448
}
432449
}

0 commit comments

Comments
 (0)