Skip to content

Commit 8186547

Browse files
paulirwinclaude
andcommitted
Work around .NET Framework RyuJIT bug in IBufferWriter tests, #1315
The net48/net472 legs of TestOpenStringBuilderBufferWriter and TestCharTermAttributeImplBufferWriter failed because the .NET Framework x64 RyuJIT optimizer miscompiles an inline read of ReadOnlyMemory<char>.Span (from the System.Memory polyfill) when the value is held in a local inside a large method, returning an empty span even though the memory is intact. The bug is optimization-dependent and does not occur on modern .NET. Route the affected ReadOnlyMemory<char>.Span reads through small [MethodImpl(NoInlining)] helpers, which the JIT compiles correctly while preserving the original assertion intent on all target frameworks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 198ec63 commit 8186547

2 files changed

Lines changed: 75 additions & 37 deletions

File tree

src/Lucene.Net.Tests.Analysis.Common/Analysis/Util/TestOpenStringBuilderBufferWriter.cs

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,25 @@ public class TestOpenStringBuilderBufferWriter : LuceneTestCase
4040
private const char DefaultChar = (char)0;
4141
private const int DefaultBufferSize = 32; // copied from hardcoded value in OpenStringBuilder
4242

43+
// LUCENENET specific: works around a .NET Framework x64 RyuJIT bug where
44+
// ReadOnlyMemory<char>.Span returns an empty span when the property is read
45+
// inline on a local inside a large method (it only reproduces with the
46+
// System.Memory polyfill; modern .NET is unaffected). Forcing the .Span read
47+
// into its own non-inlined method makes the JIT compile it correctly. These
48+
// helpers preserve the original assertion intent (comparing the Memory view
49+
// against the Span view) on all target frameworks.
50+
[MethodImpl(MethodImplOptions.NoInlining)]
51+
private static bool SpanEqualsMemory(ReadOnlySpan<char> span, ReadOnlyMemory<char> memory)
52+
=> span.SequenceEqual(memory.Span);
53+
54+
[MethodImpl(MethodImplOptions.NoInlining)]
55+
private static bool MemoryEqualsMemory(ReadOnlyMemory<char> a, ReadOnlyMemory<char> b)
56+
=> a.Span.SequenceEqual(b.Span);
57+
58+
[MethodImpl(MethodImplOptions.NoInlining)]
59+
private static char MemoryElementAt(ReadOnlyMemory<char> memory, int index)
60+
=> memory.Span[index];
61+
4362
[Test]
4463
public void ArrayBufferWriter_Ctor()
4564
{
@@ -49,7 +68,7 @@ public void ArrayBufferWriter_Ctor()
4968
Assert.AreEqual(DefaultBufferSize, output.Capacity);
5069
Assert.AreEqual(0, output.Length);
5170
Assert.True(ReadOnlySpan<char>.Empty.SequenceEqual(output.AsSpan()));
52-
Assert.True(ReadOnlyMemory<char>.Empty.Span.SequenceEqual(output.AsMemory().Span));
71+
Assert.True(MemoryEqualsMemory(ReadOnlyMemory<char>.Empty, output.AsMemory()));
5372
}
5473

5574
{
@@ -58,7 +77,7 @@ public void ArrayBufferWriter_Ctor()
5877
Assert.True(output.Capacity >= 200);
5978
Assert.AreEqual(0, output.Length);
6079
Assert.True(ReadOnlySpan<char>.Empty.SequenceEqual(output.AsSpan()));
61-
Assert.True(ReadOnlyMemory<char>.Empty.Span.SequenceEqual(output.AsMemory().Span));
80+
Assert.True(MemoryEqualsMemory(ReadOnlyMemory<char>.Empty, output.AsMemory()));
6281
}
6382

6483
{
@@ -85,22 +104,22 @@ public void Reset()
85104
Assert.True(output.FreeCapacity < previousAvailable);
86105
Assert.True(output.Length > 0);
87106
Assert.False(ReadOnlySpan<char>.Empty.SequenceEqual(output.AsSpan()));
88-
Assert.False(ReadOnlyMemory<char>.Empty.Span.SequenceEqual(output.AsMemory().Span));
89-
Assert.True(output.AsSpan().SequenceEqual(output.AsMemory().Span));
107+
Assert.False(MemoryEqualsMemory(ReadOnlyMemory<char>.Empty, output.AsMemory()));
108+
Assert.True(SpanEqualsMemory(output.AsSpan(), output.AsMemory()));
90109

91110
ReadOnlyMemory<char> transientMemory = output.AsMemory();
92111
ReadOnlySpan<char> transientSpan = output.AsSpan();
93-
char t0 = transientMemory.Span[0];
112+
char t0 = MemoryElementAt(transientMemory, 0);
94113
char t1 = transientSpan[1];
95114
Assert.AreNotEqual(DefaultChar, t0);
96115
Assert.AreNotEqual(DefaultChar, t1);
97116
output.Reset();
98-
Assert.AreEqual(t0, transientMemory.Span[0]);
117+
Assert.AreEqual(t0, MemoryElementAt(transientMemory, 0));
99118
Assert.AreEqual(t1, transientSpan[1]);
100119

101120
Assert.AreEqual(0, output.Length);
102121
Assert.True(ReadOnlySpan<char>.Empty.SequenceEqual(output.AsSpan()));
103-
Assert.True(ReadOnlyMemory<char>.Empty.Span.SequenceEqual(output.AsMemory().Span));
122+
Assert.True(MemoryEqualsMemory(ReadOnlyMemory<char>.Empty, output.AsMemory()));
104123
Assert.AreEqual(previousAvailable, output.FreeCapacity);
105124
}
106125

@@ -131,11 +150,11 @@ public void Advance()
131150
WriteData(output, 2);
132151
ReadOnlyMemory<char> previousMemory = output.AsMemory();
133152
ReadOnlySpan<char> previousSpan = output.AsSpan();
134-
Assert.True(previousSpan.SequenceEqual(previousMemory.Span));
153+
Assert.True(SpanEqualsMemory(previousSpan, previousMemory));
135154
output.Advance(10);
136-
Assert.False(previousMemory.Span.SequenceEqual(output.AsMemory().Span));
155+
Assert.False(MemoryEqualsMemory(previousMemory, output.AsMemory()));
137156
Assert.False(previousSpan.SequenceEqual(output.AsSpan()));
138-
Assert.True(output.AsSpan().SequenceEqual(output.AsMemory().Span));
157+
Assert.True(SpanEqualsMemory(output.AsSpan(), output.AsMemory()));
139158
}
140159

141160
{
@@ -144,12 +163,12 @@ public void Advance()
144163
WriteData(output, 10);
145164
ReadOnlyMemory<char> previousMemory = output.AsMemory();
146165
ReadOnlySpan<char> previousSpan = output.AsSpan();
147-
Assert.True(previousSpan.SequenceEqual(previousMemory.Span));
166+
Assert.True(SpanEqualsMemory(previousSpan, previousMemory));
148167
Assert.Throws<InvalidOperationException>(() => output.Advance(247));
149168
output.Advance(10);
150-
Assert.False(previousMemory.Span.SequenceEqual(output.AsMemory().Span));
169+
Assert.False(MemoryEqualsMemory(previousMemory, output.AsMemory()));
151170
Assert.False(previousSpan.SequenceEqual(output.AsSpan()));
152-
Assert.True(output.AsSpan().SequenceEqual(output.AsMemory().Span));
171+
Assert.True(SpanEqualsMemory(output.AsSpan(), output.AsMemory()));
153172
}
154173
}
155174

@@ -161,12 +180,12 @@ public void AdvanceZero()
161180
Assert.AreEqual(2, output.Length);
162181
ReadOnlyMemory<char> previousMemory = output.AsMemory();
163182
ReadOnlySpan<char> previousSpan = output.AsSpan();
164-
Assert.True(previousSpan.SequenceEqual(previousMemory.Span));
183+
Assert.True(SpanEqualsMemory(previousSpan, previousMemory));
165184
output.Advance(0);
166185
Assert.AreEqual(2, output.Length);
167-
Assert.True(previousMemory.Span.SequenceEqual(output.AsMemory().Span));
186+
Assert.True(MemoryEqualsMemory(previousMemory, output.AsMemory()));
168187
Assert.True(previousSpan.SequenceEqual(output.AsSpan()));
169-
Assert.True(output.AsSpan().SequenceEqual(output.AsMemory().Span));
188+
Assert.True(SpanEqualsMemory(output.AsSpan(), output.AsMemory()));
170189
}
171190

172191
[Test]
@@ -335,7 +354,7 @@ public void GetMemoryAndSpan()
335354
WriteData(output, 2);
336355
ReadOnlyMemory<char> writtenSoFarMemory = output.AsMemory();
337356
ReadOnlySpan<char> writtenSoFar = output.AsSpan();
338-
Assert.True(writtenSoFarMemory.Span.SequenceEqual(writtenSoFar));
357+
Assert.True(SpanEqualsMemory(writtenSoFar, writtenSoFarMemory));
339358
int previousAvailable = output.FreeCapacity;
340359
Span<char> span = output.GetSpan(500);
341360
Assert.True(span.Length >= 500);

src/Lucene.Net.Tests/Analysis/TokenAttributes/TestCharTermAttributeImplBufferWriter.cs

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,25 @@ public class TestCharTermAttributeImplBufferWriter : LuceneTestCase
4242
// LUCENENET specific: 10 is MIN_BUFFER_SIZE, but it gets oversized
4343
private static readonly int DefaultBufferSize = ArrayUtil.Oversize(10, RamUsageEstimator.NUM_BYTES_CHAR);
4444

45+
// LUCENENET specific: works around a .NET Framework x64 RyuJIT bug where
46+
// ReadOnlyMemory<char>.Span returns an empty span when the property is read
47+
// inline on a local inside a large method (it only reproduces with the
48+
// System.Memory polyfill; modern .NET is unaffected). Forcing the .Span read
49+
// into its own non-inlined method makes the JIT compile it correctly. These
50+
// helpers preserve the original assertion intent (comparing the Memory view
51+
// against the Span view) on all target frameworks.
52+
[MethodImpl(MethodImplOptions.NoInlining)]
53+
private static bool SpanEqualsMemory(ReadOnlySpan<char> span, ReadOnlyMemory<char> memory)
54+
=> span.SequenceEqual(memory.Span);
55+
56+
[MethodImpl(MethodImplOptions.NoInlining)]
57+
private static bool MemoryEqualsMemory(ReadOnlyMemory<char> a, ReadOnlyMemory<char> b)
58+
=> a.Span.SequenceEqual(b.Span);
59+
60+
[MethodImpl(MethodImplOptions.NoInlining)]
61+
private static char MemoryElementAt(ReadOnlyMemory<char> memory, int index)
62+
=> memory.Span[index];
63+
4564
// LUCENENET specific: note that Clear behaves more like a Reset, which resets the position without clearing the
4665
// buffer. so this test looks a lot more like the ResetWrittenCount in ArrayBufferWriter tests, than Clear.
4766
[Test]
@@ -53,22 +72,22 @@ public void Clear()
5372
Assert.True(output.FreeCapacity < previousAvailable);
5473
Assert.True(output.Length > 0);
5574
Assert.False(ReadOnlySpan<char>.Empty.SequenceEqual(output.AsSpan()));
56-
Assert.False(ReadOnlyMemory<char>.Empty.Span.SequenceEqual(output.AsMemory().Span));
57-
Assert.True(output.AsSpan().SequenceEqual(output.AsMemory().Span));
75+
Assert.False(MemoryEqualsMemory(ReadOnlyMemory<char>.Empty, output.AsMemory()));
76+
Assert.True(SpanEqualsMemory(output.AsSpan(), output.AsMemory()));
5877

5978
ReadOnlyMemory<char> transientMemory = output.AsMemory();
6079
ReadOnlySpan<char> transientSpan = output.AsSpan();
61-
char t0 = transientMemory.Span[0];
80+
char t0 = MemoryElementAt(transientMemory, 0);
6281
char t1 = transientSpan[1];
6382
Assert.AreNotEqual(DefaultChar, t0);
6483
Assert.AreNotEqual(DefaultChar, t1);
6584
output.Clear();
66-
Assert.AreEqual(t0, transientMemory.Span[0]);
85+
Assert.AreEqual(t0, MemoryElementAt(transientMemory, 0));
6786
Assert.AreEqual(t1, transientSpan[1]);
6887

6988
Assert.AreEqual(0, output.Length);
7089
Assert.True(ReadOnlySpan<char>.Empty.SequenceEqual(output.AsSpan()));
71-
Assert.True(ReadOnlyMemory<char>.Empty.Span.SequenceEqual(output.AsMemory().Span));
90+
Assert.True(MemoryEqualsMemory(ReadOnlyMemory<char>.Empty, output.AsMemory()));
7291
Assert.AreEqual(previousAvailable, output.FreeCapacity);
7392
}
7493

@@ -82,22 +101,22 @@ public void SetLengthToZero()
82101
Assert.True(output.FreeCapacity < previousAvailable);
83102
Assert.True(output.Length > 0);
84103
Assert.False(ReadOnlySpan<char>.Empty.SequenceEqual(output.AsSpan()));
85-
Assert.False(ReadOnlyMemory<char>.Empty.Span.SequenceEqual(output.AsMemory().Span));
86-
Assert.True(output.AsSpan().SequenceEqual(output.AsMemory().Span));
104+
Assert.False(MemoryEqualsMemory(ReadOnlyMemory<char>.Empty, output.AsMemory()));
105+
Assert.True(SpanEqualsMemory(output.AsSpan(), output.AsMemory()));
87106

88107
ReadOnlyMemory<char> transientMemory = output.AsMemory();
89108
ReadOnlySpan<char> transientSpan = output.AsSpan();
90-
char t0 = transientMemory.Span[0];
109+
char t0 = MemoryElementAt(transientMemory, 0);
91110
char t1 = transientSpan[1];
92111
Assert.AreNotEqual(DefaultChar, t0);
93112
Assert.AreNotEqual(DefaultChar, t1);
94113
output.Length = 0;
95-
Assert.AreEqual(t0, transientMemory.Span[0]);
114+
Assert.AreEqual(t0, MemoryElementAt(transientMemory, 0));
96115
Assert.AreEqual(t1, transientSpan[1]);
97116

98117
Assert.AreEqual(0, output.Length);
99118
Assert.True(ReadOnlySpan<char>.Empty.SequenceEqual(output.AsSpan()));
100-
Assert.True(ReadOnlyMemory<char>.Empty.Span.SequenceEqual(output.AsMemory().Span));
119+
Assert.True(MemoryEqualsMemory(ReadOnlyMemory<char>.Empty, output.AsMemory()));
101120
Assert.AreEqual(previousAvailable, output.FreeCapacity);
102121
}
103122

@@ -128,11 +147,11 @@ public void Advance()
128147
WriteData(output, 2);
129148
ReadOnlyMemory<char> previousMemory = output.AsMemory();
130149
ReadOnlySpan<char> previousSpan = output.AsSpan();
131-
Assert.True(previousSpan.SequenceEqual(previousMemory.Span));
150+
Assert.True(SpanEqualsMemory(previousSpan, previousMemory));
132151
output.Advance(10);
133-
Assert.False(previousMemory.Span.SequenceEqual(output.AsMemory().Span));
152+
Assert.False(MemoryEqualsMemory(previousMemory, output.AsMemory()));
134153
Assert.False(previousSpan.SequenceEqual(output.AsSpan()));
135-
Assert.True(output.AsSpan().SequenceEqual(output.AsMemory().Span));
154+
Assert.True(SpanEqualsMemory(output.AsSpan(), output.AsMemory()));
136155
}
137156

138157
{
@@ -141,12 +160,12 @@ public void Advance()
141160
WriteData(output, 10);
142161
ReadOnlyMemory<char> previousMemory = output.AsMemory();
143162
ReadOnlySpan<char> previousSpan = output.AsSpan();
144-
Assert.True(previousSpan.SequenceEqual(previousMemory.Span));
163+
Assert.True(SpanEqualsMemory(previousSpan, previousMemory));
145164
Assert.Throws<InvalidOperationException>(() => output.Advance(247));
146165
output.Advance(10);
147-
Assert.False(previousMemory.Span.SequenceEqual(output.AsMemory().Span));
166+
Assert.False(MemoryEqualsMemory(previousMemory, output.AsMemory()));
148167
Assert.False(previousSpan.SequenceEqual(output.AsSpan()));
149-
Assert.True(output.AsSpan().SequenceEqual(output.AsMemory().Span));
168+
Assert.True(SpanEqualsMemory(output.AsSpan(), output.AsMemory()));
150169
}
151170
}
152171

@@ -158,12 +177,12 @@ public void AdvanceZero()
158177
Assert.AreEqual(2, output.Length);
159178
ReadOnlyMemory<char> previousMemory = output.AsMemory();
160179
ReadOnlySpan<char> previousSpan = output.AsSpan();
161-
Assert.True(previousSpan.SequenceEqual(previousMemory.Span));
180+
Assert.True(SpanEqualsMemory(previousSpan, previousMemory));
162181
output.Advance(0);
163182
Assert.AreEqual(2, output.Length);
164-
Assert.True(previousMemory.Span.SequenceEqual(output.AsMemory().Span));
183+
Assert.True(MemoryEqualsMemory(previousMemory, output.AsMemory()));
165184
Assert.True(previousSpan.SequenceEqual(output.AsSpan()));
166-
Assert.True(output.AsSpan().SequenceEqual(output.AsMemory().Span));
185+
Assert.True(SpanEqualsMemory(output.AsSpan(), output.AsMemory()));
167186
}
168187

169188
[Test]
@@ -291,7 +310,7 @@ public void GetMemoryAndSpan()
291310
WriteData(output, 2);
292311
ReadOnlyMemory<char> writtenSoFarMemory = output.AsMemory();
293312
ReadOnlySpan<char> writtenSoFar = output.AsSpan();
294-
Assert.True(writtenSoFarMemory.Span.SequenceEqual(writtenSoFar));
313+
Assert.True(SpanEqualsMemory(writtenSoFar, writtenSoFarMemory));
295314
int previousAvailable = output.FreeCapacity;
296315
Span<char> span = output.GetSpan(500);
297316
Assert.True(span.Length >= 500);

0 commit comments

Comments
 (0)