Skip to content

v13.0.1 — Memory & CPU optimizations

Latest

Choose a tag to compare

@imperugo imperugo released this 03 Aug 14:34
· 10 commits to master since this release
45ded5e

Patch release: full-codebase performance pass with zero public API changes, plus three bug fixes found during the audit. Compatible drop-in upgrade from v13.0.0.

⚡ Performance

Connection pool (hot path of every operation)

  • LeastLoaded no longer re-evaluates TotalOutstanding() per comparison — each call allocated a ServerCounters snapshot in SE.Redis, up to ~9 per single Redis operation with the default pool size. Now at most one call per pooled connection.
  • Debug log arguments are no longer evaluated when logging is disabled (one more GetCounters() saved per operation).
  • RoundRobin now uses a lock-free Interlocked counter: previously it used a cryptographic RNG on net6+ and allocated new Random() per call on netstandard2.1 — and wasn't actually round-robin.
  • Removed a static lock held during blocking connection creation that serialized pool startup process-wide with multiple named clients.

Core

  • Db0..Db16 / GetDefaultDatabase() no longer allocate a new RedisDatabase on every access — instances are cached per database number.
  • HashGetAsync(hashKey, string[]) issues a single HMGET instead of N parallel HGET round-trips.
  • UpdateExpiryAsync drops the redundant EXISTS pre-check before EXPIRE — half the round-trips, N-fold for UpdateExpiryAllAsync.
  • AddAllAsync builds its payload in one pre-sized pass instead of a LINQ pipeline with growth buffers.
  • SetPopAsync(count), SortedSetRangeByScoreAsync, HashKeysAsync, SearchKeysAsync now materialize results eagerly — previously a lazy Select re-deserialized (or re-allocated) everything on each enumeration.
  • Tag writes serialize the key once instead of once per tag.
  • Pub/Sub message handling drops ~half its per-message allocations (ContinueWith continuation removed).

Serializers

  • Newtonsoft and ServiceStack no longer materialize every payload as an intermediate UTF-16 string (~2× the bytes, LOH for large payloads); Newtonsoft also caches its JsonSerializer and pools char buffers.
  • MsgPack and Protobuf drop their wrapping MemoryStreams.
  • System.Text.Json, MemoryPack, Utf8Json were already optimal.

Compressors

  • Snappier: Decompress no longer duplicates the output array (the copy was 100% redundant); Compress rents its transient worst-case buffer from ArrayPool.
  • Brotli: one-shot BrotliEncoder.TryCompress replaces the stream pipeline.
  • ZstdSharp: compression contexts are now reused per thread instead of being created per call — the main zstd anti-pattern — plus exact-size decompression.
  • GZip: pre-sized buffers.

ASP.NET Core

  • IDistributedCache adapter: cached field arrays (no per-Get allocation) and batched HSET+EXPIRE in a single network write on SetAsync.

🐛 Bug fixes

  • HashGetAllAsyncAtOneTimeAsync with KeyPrefix: the per-call Lua script embedded the hash key unprefixed while wrongly prefixing the hash fields — with a prefix configured the method read the wrong hash. The script was also a Lua injection vector and defeated the server-side script cache. Replaced by HMGET.
  • Utf8JsonSerializer.Deserialize(null) threw instead of returning default, unlike every other serializer.
  • ListGetFromRightAsync had a duplicated null check (dead code).

Behavioral notes

  • HashGetAsync(string[]) returns a Dictionary instead of ConcurrentDictionary (contract is IDictionary<string, T?>).
  • RoundRobin selection is now sequential instead of random.
  • Pub/Sub handler errors log the original exception instead of the wrapping AggregateException.

Full changelog: v13.0.0...v13.0.1