bitfaster / BitFaster.Caching

High performance, thread-safe in-memory caching primitives for .NET
MIT License
466 stars 28 forks source link

Mitigate LRU struct tearing using SeqLock #593

Closed bitfaster closed 4 months ago

bitfaster commented 4 months ago

If the cache entry is a value type larger than the native pointer size (e.g. a Guid), writes are not atomic and if the value is updated and read concurrently, readers may see a torn value.

There are at least two ways to solve this:

  1. Lock the item containing the value on read. This PR implements option 1 using a SeqLock.
  2. Make a new LruItem and update the dictionary. See PR #545.

Option 1 is preferred, because option 2 can make cache size unstable (stale values consume queue slots, pushing out live cache entries).

SeqLock pros and cons:

Atomic/Scoped etc.

The update code paths for atomic/scoped caches generate new wrapper class instances and call cache update to replace the object. They are therefore not susceptible to torn reads - the structs inside them are not changed after the wrapper is created.

Using a lock statement (https://github.com/bitfaster/BitFaster.Caching/pull/593/commits/be8f0ed835b24bf6303adb470ec531d4c3415ddb)

Naive implementation using a C# lock statement added to read makes LRU roughly the same latency as LFU with a Guid value. Since LruItem is already locked on update, torn reads are prevented by the lock. This comes with the overhead of the lock, which results in lock contention for concurrent reads.

BitFaster Caching Benchmarks LruJustGetOrAddGuid- NET 6 0-columnchart BitFaster Caching Benchmarks LruJustGetOrAddGuid- NET Framework 4 8-columnchart

Read throughput is significantly reduced: Results_Read_500

Using SeqLock (https://github.com/bitfaster/BitFaster.Caching/pull/593/commits/87fcd06c3e991314ed3ca25387403b19b62b7850)

See SeqLock. This is a good fit for our scenario, because we already have a single threaded update, and we can keep reads lock free and fast.

BitFaster Caching Benchmarks LruJustGetOrAddGuid- NET 6 0-columnchart BitFaster Caching Benchmarks LruJustGetOrAddGuid- NET Framework 4 8-columnchart Results_Read_500 Results_ReadWrite_500

coveralls commented 4 months ago

Coverage Status

coverage: 99.141% (+0.01%) from 99.13% when pulling 7a88edf95aa0f49b88c88d138ce816517ec89c5c on users/alexpeck/tornwrite2 into 8934324e1b30ca0ea4f14eb2d9e0c2bbe77bb78e on main.