Skip to content

Commit 8eb06ae

Browse files
committed
perf(domain): AbstractValueObject 배열 비교를 Span SequenceEqual / HashCode API로 최적화
ValueObjectEqualityComparer가 모든 배열 element를 Array.GetValue(int)로 접근 + value type → object boxing + 재귀 Equals 호출 → 매 비교마다 O(n) 시간 + O(n) heap 할당 발생. 16-bit Hash, byte[] 서명, Guid[] ID 등 흔한 ValueObject 패턴에서 측정 가능한 GC pressure 유발. 변경 (Tier 1+2+3, 모두 적용): - Tier 1: byte[]에 SIMD-friendly Span<byte>.SequenceEqual 빠른 경로 - Tier 2: 다른 primitive types (sbyte/short/ushort/int/uint/long/ulong/ float/double/char/bool/Guid/decimal/string) 동일 패턴 - Tier 3: GetHashCode도 type-specific 빠른 경로 — byte[]는 HashCode.AddBytes (SIMD), 그 외 primitive는 generic HashCode.Add<T> (no boxing) - 폴백 경로(다차원/jagged/custom 객체): 기존 element-wise 유지 신설: Tests.Benchmarks/AbstractValueObject.Benchmarks/ - BenchmarkDotNet 전용 프로젝트, Benchmarks.slnx 등록 - ArrayEqualityBenchmarks: byte/int/long/Guid/string × 16/256/4096 - ArrayHashCodeBenchmarks: byte/int/Guid × 16/256/4096 - README.md에 Before/After 측정 결과 표 실측 결과(Windows 11, i7-1065G7, .NET 10.0.2 x64, AVX2): Equals: - byte[] 4096: 116,000ns → 82ns (1,414× 빠름, 192KB→0) - byte[] 256: 6,904ns → 5.9ns (1,170×, 12KB→0) - byte[] 16: 466ns → 1.3ns (351×, 768B→0) - int[] 4096: 100,200ns → 288ns (348×, 192KB→0) - long[] 4096: 100,500ns → 1,453ns (69×, 192KB→0) - Guid[] 4096: 146,300ns → 4,409ns (33×, 256KB→0) - string[] 4096: 20,870ns → 6,514ns (3.2×, 0→0) GetHashCode: - byte[] 4096: 47,500ns → 901ns (53×, 96KB→0) - byte[] 256: 3,057ns → 69ns (44×, 6KB→0) - int[] 4096: 51,975ns → 6,614ns (8×, 96KB→0) - Guid[] 4096: 57,400ns → 10,470ns (5.5×, 128KB→0) 핵심: 모든 primitive array에서 boxing 100% 제거. 24/365 가동 시스템에서 GC pause 빈도/tail-latency 직접 개선. 검증: Functorium.slnx 빌드 0 오류, 1556/1584 테스트 통과(28 skip). Benchmarks.slnx 빌드 0 오류.
1 parent 5a9eb59 commit 8eb06ae

7 files changed

Lines changed: 441 additions & 8 deletions

File tree

Src/Functorium/Domains/ValueObjects/AbstractValueObject.cs

Lines changed: 129 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,16 +146,16 @@ private static bool IsProxyType(Type type)
146146
}
147147

148148
/// <summary>
149-
/// 값 객체 동등성 비교를 위한 커스텀 EqualityComparer
150-
/// 배열 타입에 대해 요소별 내용 비교를 수행
149+
/// 값 객체 동등성 비교를 위한 커스텀 EqualityComparer.
150+
/// primitive array에 대해 SIMD 최적화된 Span&lt;T&gt;.SequenceEqual을 사용하고,
151+
/// 그 외 array는 element-wise 폴백 경로를 사용합니다.
151152
/// </summary>
152153
/// <remarks>
153-
/// 성능 고려사항:
154-
/// - 배열 비교는 O(n) 시간 복잡도를 가짐
155-
/// - 대용량 배열(100KB 이상)에는 적합하지 않음
156-
/// 예: byte[] 102,400건, int[] 25,600건, Guid[] 6,400건
157-
/// - 해시코드는 캐시되므로 첫 계산 이후 O(1)
158-
/// - 작은 배열(해시값, 서명 등)에 최적화된 구현
154+
/// 성능 특성:
155+
/// - primitive array(byte/int/long/Guid 등): SIMD 가속(AVX2/SSE2)으로 KB 단위 비교를 수십~수백 ns에 처리.
156+
/// 이전 구현(Array.GetValue + boxing) 대비 50-500배 빠름, GC pressure 0.
157+
/// - 그 외 array(다차원/jagged/custom): 폴백 경로로 element-wise 비교(boxing 발생).
158+
/// - 해시코드는 AbstractValueObject._cachedHashCode로 1회 계산 후 캐시.
159159
/// </remarks>
160160
private sealed class ValueObjectEqualityComparer : IEqualityComparer<object>
161161
{
@@ -177,6 +177,31 @@ private ValueObjectEqualityComparer() { }
177177
if (xArray.Length != yArray.Length)
178178
return false;
179179

180+
// Tier 1·2 — primitive array 빠른 경로
181+
// (SIMD 가속 Span.SequenceEqual, boxing/GetValue 회피)
182+
if (x.GetType() == y.GetType())
183+
{
184+
switch (x)
185+
{
186+
case byte[] xb: return xb.AsSpan().SequenceEqual((byte[])y);
187+
case sbyte[] xsb: return xsb.AsSpan().SequenceEqual((sbyte[])y);
188+
case short[] xsh: return xsh.AsSpan().SequenceEqual((short[])y);
189+
case ushort[] xush: return xush.AsSpan().SequenceEqual((ushort[])y);
190+
case int[] xi: return xi.AsSpan().SequenceEqual((int[])y);
191+
case uint[] xui: return xui.AsSpan().SequenceEqual((uint[])y);
192+
case long[] xl: return xl.AsSpan().SequenceEqual((long[])y);
193+
case ulong[] xul: return xul.AsSpan().SequenceEqual((ulong[])y);
194+
case float[] xf: return xf.AsSpan().SequenceEqual((float[])y);
195+
case double[] xd: return xd.AsSpan().SequenceEqual((double[])y);
196+
case char[] xc: return xc.AsSpan().SequenceEqual((char[])y);
197+
case bool[] xbo: return xbo.AsSpan().SequenceEqual((bool[])y);
198+
case Guid[] xg: return xg.AsSpan().SequenceEqual((Guid[])y);
199+
case decimal[] xde: return xde.AsSpan().SequenceEqual((decimal[])y);
200+
case string[] xs: return xs.AsSpan().SequenceEqual((string[])y);
201+
}
202+
}
203+
204+
// 폴백: 그 외 array(다차원/jagged/custom 객체 등)는 element-wise 비교
180205
for (int i = 0; i < xArray.Length; i++)
181206
{
182207
if (!Equals(xArray.GetValue(i), yArray.GetValue(i)))
@@ -192,6 +217,102 @@ public int GetHashCode(object obj)
192217
{
193218
if (obj is Array array)
194219
{
220+
// Tier 3 — primitive array 빠른 경로 (boxing 회피)
221+
switch (obj)
222+
{
223+
case byte[] bytes:
224+
{
225+
var hc = new HashCode();
226+
hc.AddBytes(bytes);
227+
return hc.ToHashCode();
228+
}
229+
case sbyte[] sbytes:
230+
{
231+
var hc = new HashCode();
232+
foreach (sbyte v in sbytes) hc.Add(v);
233+
return hc.ToHashCode();
234+
}
235+
case short[] shorts:
236+
{
237+
var hc = new HashCode();
238+
foreach (short v in shorts) hc.Add(v);
239+
return hc.ToHashCode();
240+
}
241+
case ushort[] ushorts:
242+
{
243+
var hc = new HashCode();
244+
foreach (ushort v in ushorts) hc.Add(v);
245+
return hc.ToHashCode();
246+
}
247+
case int[] ints:
248+
{
249+
var hc = new HashCode();
250+
foreach (int v in ints) hc.Add(v);
251+
return hc.ToHashCode();
252+
}
253+
case uint[] uints:
254+
{
255+
var hc = new HashCode();
256+
foreach (uint v in uints) hc.Add(v);
257+
return hc.ToHashCode();
258+
}
259+
case long[] longs:
260+
{
261+
var hc = new HashCode();
262+
foreach (long v in longs) hc.Add(v);
263+
return hc.ToHashCode();
264+
}
265+
case ulong[] ulongs:
266+
{
267+
var hc = new HashCode();
268+
foreach (ulong v in ulongs) hc.Add(v);
269+
return hc.ToHashCode();
270+
}
271+
case float[] floats:
272+
{
273+
var hc = new HashCode();
274+
foreach (float v in floats) hc.Add(v);
275+
return hc.ToHashCode();
276+
}
277+
case double[] doubles:
278+
{
279+
var hc = new HashCode();
280+
foreach (double v in doubles) hc.Add(v);
281+
return hc.ToHashCode();
282+
}
283+
case char[] chars:
284+
{
285+
var hc = new HashCode();
286+
foreach (char v in chars) hc.Add(v);
287+
return hc.ToHashCode();
288+
}
289+
case bool[] bools:
290+
{
291+
var hc = new HashCode();
292+
foreach (bool v in bools) hc.Add(v);
293+
return hc.ToHashCode();
294+
}
295+
case Guid[] guids:
296+
{
297+
var hc = new HashCode();
298+
foreach (Guid v in guids) hc.Add(v);
299+
return hc.ToHashCode();
300+
}
301+
case decimal[] decimals:
302+
{
303+
var hc = new HashCode();
304+
foreach (decimal v in decimals) hc.Add(v);
305+
return hc.ToHashCode();
306+
}
307+
case string[] strings:
308+
{
309+
var hc = new HashCode();
310+
foreach (string? v in strings) hc.Add(v);
311+
return hc.ToHashCode();
312+
}
313+
}
314+
315+
// 폴백: 그 외 array(다차원/jagged/custom 객체 등)
195316
unchecked
196317
{
197318
int hash = 17;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<LangVersion>latest</LangVersion>
9+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="BenchmarkDotNet" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<ProjectReference Include="$(FunctoriumSrcRoot)\Functorium\Functorium.csproj" />
18+
</ItemGroup>
19+
20+
</Project>
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using BenchmarkDotNet.Attributes;
2+
3+
namespace AbstractValueObject.Benchmarks;
4+
5+
/// <summary>
6+
/// AbstractValueObject 내부 ValueObjectEqualityComparer의 배열 비교 패턴을 비교합니다.
7+
///
8+
/// Old: Array.GetValue(int) + boxing + 재귀 Equals (현재 코드 패턴)
9+
/// New: type-specific Span&lt;T&gt;.SequenceEqual (SIMD 가속)
10+
///
11+
/// byte[]·int[]·long[]·Guid[]·string[] 다양한 크기에서 측정합니다.
12+
/// </summary>
13+
[MemoryDiagnoser]
14+
[ShortRunJob]
15+
public class ArrayEqualityBenchmarks
16+
{
17+
[Params(16, 256, 4096)]
18+
public int ArraySize { get; set; }
19+
20+
private byte[] _byteA = null!;
21+
private byte[] _byteB = null!;
22+
private int[] _intA = null!;
23+
private int[] _intB = null!;
24+
private long[] _longA = null!;
25+
private long[] _longB = null!;
26+
private Guid[] _guidA = null!;
27+
private Guid[] _guidB = null!;
28+
private string[] _stringA = null!;
29+
private string[] _stringB = null!;
30+
31+
[GlobalSetup]
32+
public void Setup()
33+
{
34+
var rng = new Random(42);
35+
36+
_byteA = new byte[ArraySize];
37+
rng.NextBytes(_byteA);
38+
_byteB = (byte[])_byteA.Clone();
39+
40+
_intA = new int[ArraySize];
41+
for (int i = 0; i < ArraySize; i++) _intA[i] = rng.Next();
42+
_intB = (int[])_intA.Clone();
43+
44+
_longA = new long[ArraySize];
45+
for (int i = 0; i < ArraySize; i++) _longA[i] = rng.NextInt64();
46+
_longB = (long[])_longA.Clone();
47+
48+
_guidA = new Guid[ArraySize];
49+
for (int i = 0; i < ArraySize; i++) _guidA[i] = Guid.NewGuid();
50+
_guidB = (Guid[])_guidA.Clone();
51+
52+
_stringA = new string[ArraySize];
53+
for (int i = 0; i < ArraySize; i++) _stringA[i] = $"item_{i}";
54+
_stringB = (string[])_stringA.Clone();
55+
}
56+
57+
// ─── byte[] ────────────────────────────────────────
58+
59+
[Benchmark(Baseline = true), BenchmarkCategory("byte[]")]
60+
public bool Old_ByteArray() => OldArrayEquals(_byteA, _byteB);
61+
62+
[Benchmark, BenchmarkCategory("byte[]")]
63+
public bool New_ByteArray() => _byteA.AsSpan().SequenceEqual(_byteB);
64+
65+
// ─── int[] ────────────────────────────────────────
66+
67+
[Benchmark, BenchmarkCategory("int[]")]
68+
public bool Old_IntArray() => OldArrayEquals(_intA, _intB);
69+
70+
[Benchmark, BenchmarkCategory("int[]")]
71+
public bool New_IntArray() => _intA.AsSpan().SequenceEqual(_intB);
72+
73+
// ─── long[] ───────────────────────────────────────
74+
75+
[Benchmark, BenchmarkCategory("long[]")]
76+
public bool Old_LongArray() => OldArrayEquals(_longA, _longB);
77+
78+
[Benchmark, BenchmarkCategory("long[]")]
79+
public bool New_LongArray() => _longA.AsSpan().SequenceEqual(_longB);
80+
81+
// ─── Guid[] ───────────────────────────────────────
82+
83+
[Benchmark, BenchmarkCategory("Guid[]")]
84+
public bool Old_GuidArray() => OldArrayEquals(_guidA, _guidB);
85+
86+
[Benchmark, BenchmarkCategory("Guid[]")]
87+
public bool New_GuidArray() => _guidA.AsSpan().SequenceEqual(_guidB);
88+
89+
// ─── string[] ─────────────────────────────────────
90+
91+
[Benchmark, BenchmarkCategory("string[]")]
92+
public bool Old_StringArray() => OldArrayEquals(_stringA, _stringB);
93+
94+
[Benchmark, BenchmarkCategory("string[]")]
95+
public bool New_StringArray() => _stringA.AsSpan().SequenceEqual(_stringB);
96+
97+
// ─── 기존 패턴 (GetValue + boxing + 재귀 Equals) ───────
98+
99+
private static bool OldArrayEquals(Array xArray, Array yArray)
100+
{
101+
if (xArray.Length != yArray.Length)
102+
return false;
103+
104+
for (int i = 0; i < xArray.Length; i++)
105+
{
106+
object? xv = xArray.GetValue(i);
107+
object? yv = yArray.GetValue(i);
108+
109+
if (xv is null && yv is null) continue;
110+
if (xv is null || yv is null) return false;
111+
if (!xv.Equals(yv)) return false;
112+
}
113+
return true;
114+
}
115+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using BenchmarkDotNet.Attributes;
2+
3+
namespace AbstractValueObject.Benchmarks;
4+
5+
/// <summary>
6+
/// AbstractValueObject 내부 ValueObjectEqualityComparer의 배열 해시 패턴을 비교합니다.
7+
///
8+
/// Old: foreach (var item in array) → IEnumerator → boxing → item.GetHashCode()
9+
/// New: type-specific HashCode.AddBytes / generic Add&lt;T&gt; (boxing 회피)
10+
/// </summary>
11+
[MemoryDiagnoser]
12+
[ShortRunJob]
13+
public class ArrayHashCodeBenchmarks
14+
{
15+
[Params(16, 256, 4096)]
16+
public int ArraySize { get; set; }
17+
18+
private byte[] _bytes = null!;
19+
private int[] _ints = null!;
20+
private Guid[] _guids = null!;
21+
22+
[GlobalSetup]
23+
public void Setup()
24+
{
25+
var rng = new Random(42);
26+
27+
_bytes = new byte[ArraySize];
28+
rng.NextBytes(_bytes);
29+
30+
_ints = new int[ArraySize];
31+
for (int i = 0; i < ArraySize; i++) _ints[i] = rng.Next();
32+
33+
_guids = new Guid[ArraySize];
34+
for (int i = 0; i < ArraySize; i++) _guids[i] = Guid.NewGuid();
35+
}
36+
37+
// ─── byte[] ────────────────────────────────────────
38+
39+
[Benchmark(Baseline = true), BenchmarkCategory("byte[]")]
40+
public int Old_ByteArrayHash() => OldArrayHash(_bytes);
41+
42+
[Benchmark, BenchmarkCategory("byte[]")]
43+
public int New_ByteArrayHash()
44+
{
45+
var hc = new HashCode();
46+
hc.AddBytes(_bytes);
47+
return hc.ToHashCode();
48+
}
49+
50+
// ─── int[] ────────────────────────────────────────
51+
52+
[Benchmark, BenchmarkCategory("int[]")]
53+
public int Old_IntArrayHash() => OldArrayHash(_ints);
54+
55+
[Benchmark, BenchmarkCategory("int[]")]
56+
public int New_IntArrayHash()
57+
{
58+
var hc = new HashCode();
59+
foreach (int i in _ints) hc.Add(i);
60+
return hc.ToHashCode();
61+
}
62+
63+
// ─── Guid[] ───────────────────────────────────────
64+
65+
[Benchmark, BenchmarkCategory("Guid[]")]
66+
public int Old_GuidArrayHash() => OldArrayHash(_guids);
67+
68+
[Benchmark, BenchmarkCategory("Guid[]")]
69+
public int New_GuidArrayHash()
70+
{
71+
var hc = new HashCode();
72+
foreach (Guid g in _guids) hc.Add(g);
73+
return hc.ToHashCode();
74+
}
75+
76+
// ─── 기존 패턴 (foreach + boxing + GetHashCode) ───────
77+
78+
private static int OldArrayHash(Array array)
79+
{
80+
unchecked
81+
{
82+
int hash = 17;
83+
foreach (var item in array)
84+
{
85+
hash = hash * 23 + (item?.GetHashCode() ?? 0);
86+
}
87+
return hash;
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)