Skip to content

Commit 0043b6e

Browse files
authored
Rewrite SpanHelpers.IndexOfNullByte/IndexOfNullCharacter to unmanaged pointers (#81347)
1 parent e831dab commit 0043b6e

File tree

3 files changed

+48
-68
lines changed

3 files changed

+48
-68
lines changed

src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Byte.cs

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ private static void ThrowMustBeNullTerminatedString()
343343
// IndexOfNullByte processes memory in aligned chunks, and thus it won't crash even if it accesses memory beyond the null terminator.
344344
// This behavior is an implementation detail of the runtime and callers outside System.Private.CoreLib must not depend on it.
345345
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
346-
internal static unsafe int IndexOfNullByte(ref byte searchSpace)
346+
internal static unsafe int IndexOfNullByte(byte* searchSpace)
347347
{
348348
const int Length = int.MaxValue;
349349

@@ -354,32 +354,32 @@ internal static unsafe int IndexOfNullByte(ref byte searchSpace)
354354
if (Vector128.IsHardwareAccelerated)
355355
{
356356
// Avx2 branch also operates on Sse2 sizes, so check is combined.
357-
lengthToExamine = UnalignedCountVector128(ref searchSpace);
357+
lengthToExamine = UnalignedCountVector128(searchSpace);
358358
}
359359
else if (Vector.IsHardwareAccelerated)
360360
{
361-
lengthToExamine = UnalignedCountVector(ref searchSpace);
361+
lengthToExamine = UnalignedCountVector(searchSpace);
362362
}
363363
SequentialScan:
364364
while (lengthToExamine >= 8)
365365
{
366366
lengthToExamine -= 8;
367367

368-
if (uValue == Unsafe.AddByteOffset(ref searchSpace, offset))
368+
if (uValue == searchSpace[offset])
369369
goto Found;
370-
if (uValue == Unsafe.AddByteOffset(ref searchSpace, offset + 1))
370+
if (uValue == searchSpace[offset + 1])
371371
goto Found1;
372-
if (uValue == Unsafe.AddByteOffset(ref searchSpace, offset + 2))
372+
if (uValue == searchSpace[offset + 2])
373373
goto Found2;
374-
if (uValue == Unsafe.AddByteOffset(ref searchSpace, offset + 3))
374+
if (uValue == searchSpace[offset + 3])
375375
goto Found3;
376-
if (uValue == Unsafe.AddByteOffset(ref searchSpace, offset + 4))
376+
if (uValue == searchSpace[offset + 4])
377377
goto Found4;
378-
if (uValue == Unsafe.AddByteOffset(ref searchSpace, offset + 5))
378+
if (uValue == searchSpace[offset + 5])
379379
goto Found5;
380-
if (uValue == Unsafe.AddByteOffset(ref searchSpace, offset + 6))
380+
if (uValue == searchSpace[offset + 6])
381381
goto Found6;
382-
if (uValue == Unsafe.AddByteOffset(ref searchSpace, offset + 7))
382+
if (uValue == searchSpace[offset + 7])
383383
goto Found7;
384384

385385
offset += 8;
@@ -389,13 +389,13 @@ internal static unsafe int IndexOfNullByte(ref byte searchSpace)
389389
{
390390
lengthToExamine -= 4;
391391

392-
if (uValue == Unsafe.AddByteOffset(ref searchSpace, offset))
392+
if (uValue == searchSpace[offset])
393393
goto Found;
394-
if (uValue == Unsafe.AddByteOffset(ref searchSpace, offset + 1))
394+
if (uValue == searchSpace[offset + 1])
395395
goto Found1;
396-
if (uValue == Unsafe.AddByteOffset(ref searchSpace, offset + 2))
396+
if (uValue == searchSpace[offset + 2])
397397
goto Found2;
398-
if (uValue == Unsafe.AddByteOffset(ref searchSpace, offset + 3))
398+
if (uValue == searchSpace[offset + 3])
399399
goto Found3;
400400

401401
offset += 4;
@@ -405,7 +405,7 @@ internal static unsafe int IndexOfNullByte(ref byte searchSpace)
405405
{
406406
lengthToExamine -= 1;
407407

408-
if (uValue == Unsafe.AddByteOffset(ref searchSpace, offset))
408+
if (uValue == searchSpace[offset])
409409
goto Found;
410410

411411
offset += 1;
@@ -418,13 +418,13 @@ internal static unsafe int IndexOfNullByte(ref byte searchSpace)
418418
{
419419
if (offset < (nuint)(uint)Length)
420420
{
421-
if ((((nuint)(uint)Unsafe.AsPointer(ref searchSpace) + offset) & (nuint)(Vector256<byte>.Count - 1)) != 0)
421+
if ((((nuint)(uint)searchSpace + offset) & (nuint)(Vector256<byte>.Count - 1)) != 0)
422422
{
423423
// Not currently aligned to Vector256 (is aligned to Vector128); this can cause a problem for searches
424424
// with no upper bound e.g. String.strlen.
425425
// Start with a check on Vector128 to align to Vector256, before moving to processing Vector256.
426426
// This ensures we do not fault across memory pages while searching for an end of string.
427-
Vector128<byte> search = Vector128.LoadUnsafe(ref searchSpace, offset);
427+
Vector128<byte> search = Vector128.Load(searchSpace + offset);
428428

429429
// Same method as below
430430
uint matches = Vector128.Equals(Vector128<byte>.Zero, search).ExtractMostSignificantBits();
@@ -445,7 +445,7 @@ internal static unsafe int IndexOfNullByte(ref byte searchSpace)
445445
{
446446
do
447447
{
448-
Vector256<byte> search = Vector256.LoadUnsafe(ref searchSpace, offset);
448+
Vector256<byte> search = Vector256.Load(searchSpace + offset);
449449
uint matches = Vector256.Equals(Vector256<byte>.Zero, search).ExtractMostSignificantBits();
450450
// Note that MoveMask has converted the equal vector elements into a set of bit flags,
451451
// So the bit position in 'matches' corresponds to the element offset.
@@ -464,7 +464,7 @@ internal static unsafe int IndexOfNullByte(ref byte searchSpace)
464464
lengthToExamine = GetByteVector128SpanLength(offset, Length);
465465
if (lengthToExamine > offset)
466466
{
467-
Vector128<byte> search = Vector128.LoadUnsafe(ref searchSpace, offset);
467+
Vector128<byte> search = Vector128.Load(searchSpace + offset);
468468

469469
// Same method as above
470470
uint matches = Vector128.Equals(Vector128<byte>.Zero, search).ExtractMostSignificantBits();
@@ -495,7 +495,7 @@ internal static unsafe int IndexOfNullByte(ref byte searchSpace)
495495

496496
while (lengthToExamine > offset)
497497
{
498-
Vector128<byte> search = Vector128.LoadUnsafe(ref searchSpace, offset);
498+
Vector128<byte> search = Vector128.Load(searchSpace + offset);
499499

500500
// Same method as above
501501
Vector128<byte> compareResult = Vector128.Equals(Vector128<byte>.Zero, search);
@@ -526,7 +526,7 @@ internal static unsafe int IndexOfNullByte(ref byte searchSpace)
526526

527527
while (lengthToExamine > offset)
528528
{
529-
var matches = Vector.Equals(Vector<byte>.Zero, LoadVector(ref searchSpace, offset));
529+
Vector<byte> matches = Vector.Equals(Vector<byte>.Zero, Vector.Load(searchSpace + offset));
530530
if (Vector<byte>.Zero.Equals(matches))
531531
{
532532
offset += (nuint)Vector<byte>.Count;
@@ -1123,16 +1123,16 @@ private static nuint GetByteVector256SpanLength(nuint offset, int length)
11231123
=> (nuint)(uint)((length - (int)offset) & ~(Vector256<byte>.Count - 1));
11241124

11251125
[MethodImpl(MethodImplOptions.AggressiveInlining)]
1126-
private static unsafe nuint UnalignedCountVector(ref byte searchSpace)
1126+
private static unsafe nuint UnalignedCountVector(byte* searchSpace)
11271127
{
1128-
nint unaligned = (nint)Unsafe.AsPointer(ref searchSpace) & (Vector<byte>.Count - 1);
1128+
nint unaligned = (nint)searchSpace & (Vector<byte>.Count - 1);
11291129
return (nuint)((Vector<byte>.Count - unaligned) & (Vector<byte>.Count - 1));
11301130
}
11311131

11321132
[MethodImpl(MethodImplOptions.AggressiveInlining)]
1133-
private static unsafe nuint UnalignedCountVector128(ref byte searchSpace)
1133+
private static unsafe nuint UnalignedCountVector128(byte* searchSpace)
11341134
{
1135-
nint unaligned = (nint)Unsafe.AsPointer(ref searchSpace) & (Vector128<byte>.Count - 1);
1135+
nint unaligned = (nint)searchSpace & (Vector128<byte>.Count - 1);
11361136
return (nuint)(uint)((Vector128<byte>.Count - unaligned) & (Vector128<byte>.Count - 1));
11371137
}
11381138

src/libraries/System.Private.CoreLib/src/System/SpanHelpers.Char.cs

Lines changed: 20 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -425,28 +425,28 @@ public static unsafe int SequenceCompareTo(ref char first, int firstLength, ref
425425
// IndexOfNullCharacter processes memory in aligned chunks, and thus it won't crash even if it accesses memory beyond the null terminator.
426426
// This behavior is an implementation detail of the runtime and callers outside System.Private.CoreLib must not depend on it.
427427
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
428-
public static unsafe int IndexOfNullCharacter(ref char searchSpace)
428+
public static unsafe int IndexOfNullCharacter(char* searchSpace)
429429
{
430430
const char value = '\0';
431431
const int length = int.MaxValue;
432432

433433
nint offset = 0;
434434
nint lengthToExamine = length;
435435

436-
if (((int)Unsafe.AsPointer(ref searchSpace) & 1) != 0)
436+
if (((int)searchSpace & 1) != 0)
437437
{
438438
// Input isn't char aligned, we won't be able to align it to a Vector
439439
}
440440
else if (Vector128.IsHardwareAccelerated)
441441
{
442442
// Avx2 branch also operates on Sse2 sizes, so check is combined.
443443
// Needs to be double length to allow us to align the data first.
444-
lengthToExamine = UnalignedCountVector128(ref searchSpace);
444+
lengthToExamine = UnalignedCountVector128(searchSpace);
445445
}
446446
else if (Vector.IsHardwareAccelerated)
447447
{
448448
// Needs to be double length to allow us to align the data first.
449-
lengthToExamine = UnalignedCountVector(ref searchSpace);
449+
lengthToExamine = UnalignedCountVector(searchSpace);
450450
}
451451

452452
SequentialScan:
@@ -456,15 +456,13 @@ public static unsafe int IndexOfNullCharacter(ref char searchSpace)
456456
// remaining data that is shorter than a Vector length.
457457
while (lengthToExamine >= 4)
458458
{
459-
ref char current = ref Unsafe.Add(ref searchSpace, offset);
460-
461-
if (value == current)
459+
if (value == searchSpace[offset])
462460
goto Found;
463-
if (value == Unsafe.Add(ref current, 1))
461+
if (value == searchSpace[offset + 1])
464462
goto Found1;
465-
if (value == Unsafe.Add(ref current, 2))
463+
if (value == searchSpace[offset + 2])
466464
goto Found2;
467-
if (value == Unsafe.Add(ref current, 3))
465+
if (value == searchSpace[offset + 3])
468466
goto Found3;
469467

470468
offset += 4;
@@ -473,7 +471,7 @@ public static unsafe int IndexOfNullCharacter(ref char searchSpace)
473471

474472
while (lengthToExamine > 0)
475473
{
476-
if (value == Unsafe.Add(ref searchSpace, offset))
474+
if (value == searchSpace[offset])
477475
goto Found;
478476

479477
offset++;
@@ -487,25 +485,19 @@ public static unsafe int IndexOfNullCharacter(ref char searchSpace)
487485
if (offset < length)
488486
{
489487
Debug.Assert(length - offset >= Vector128<ushort>.Count);
490-
ref ushort ushortSearchSpace = ref Unsafe.As<char, ushort>(ref searchSpace);
491-
if (((nint)Unsafe.AsPointer(ref Unsafe.Add(ref searchSpace, (nint)offset)) & (nint)(Vector256<byte>.Count - 1)) != 0)
488+
if (((nint)(searchSpace + (nint)offset) & (nint)(Vector256<byte>.Count - 1)) != 0)
492489
{
493490
// Not currently aligned to Vector256 (is aligned to Vector128); this can cause a problem for searches
494491
// with no upper bound e.g. String.wcslen. Start with a check on Vector128 to align to Vector256,
495492
// before moving to processing Vector256.
496493

497-
// If the input searchSpan has been fixed or pinned, this ensures we do not fault across memory pages
494+
// This ensures we do not fault across memory pages
498495
// while searching for an end of string. Specifically that this assumes that the length is either correct
499496
// or that the data is pinned otherwise it may cause an AccessViolation from crossing a page boundary into an
500497
// unowned page. If the search is unbounded (e.g. null terminator in wcslen) and the search value is not found,
501498
// again this will likely cause an AccessViolation. However, correctly bounded searches will return -1 rather
502499
// than ever causing an AV.
503-
504-
// If the searchSpan has not been fixed or pinned the GC can relocate it during the execution of this
505-
// method, so the alignment only acts as best endeavour. The GC cost is likely to dominate over
506-
// the misalignment that may occur after; to we default to giving the GC a free hand to relocate and
507-
// its up to the caller whether they are operating over fixed data.
508-
Vector128<ushort> search = Vector128.LoadUnsafe(ref ushortSearchSpace, (nuint)offset);
500+
Vector128<ushort> search = *(Vector128<ushort>*)(searchSpace + (nuint)offset);
509501

510502
// Same method as below
511503
uint matches = Vector128.Equals(Vector128<ushort>.Zero, search).AsByte().ExtractMostSignificantBits();
@@ -528,7 +520,7 @@ public static unsafe int IndexOfNullCharacter(ref char searchSpace)
528520
{
529521
Debug.Assert(lengthToExamine >= Vector256<ushort>.Count);
530522

531-
Vector256<ushort> search = Vector256.LoadUnsafe(ref ushortSearchSpace, (nuint)offset);
523+
Vector256<ushort> search = *(Vector256<ushort>*)(searchSpace + (nuint)offset);
532524
uint matches = Vector256.Equals(Vector256<ushort>.Zero, search).AsByte().ExtractMostSignificantBits();
533525
// Note that MoveMask has converted the equal vector elements into a set of bit flags,
534526
// So the bit position in 'matches' corresponds to the element offset.
@@ -551,7 +543,7 @@ public static unsafe int IndexOfNullCharacter(ref char searchSpace)
551543
{
552544
Debug.Assert(lengthToExamine >= Vector128<ushort>.Count);
553545

554-
Vector128<ushort> search = Vector128.LoadUnsafe(ref ushortSearchSpace, (nuint)offset);
546+
Vector128<ushort> search = *(Vector128<ushort>*)(searchSpace + (nuint)offset);
555547

556548
// Same method as above
557549
uint matches = Vector128.Equals(Vector128<ushort>.Zero, search).AsByte().ExtractMostSignificantBits();
@@ -581,7 +573,6 @@ public static unsafe int IndexOfNullCharacter(ref char searchSpace)
581573
if (offset < length)
582574
{
583575
Debug.Assert(length - offset >= Vector128<ushort>.Count);
584-
ref ushort ushortSearchSpace = ref Unsafe.As<char, ushort>(ref searchSpace);
585576

586577
lengthToExamine = GetCharVector128SpanLength(offset, length);
587578
if (lengthToExamine > 0)
@@ -590,7 +581,7 @@ public static unsafe int IndexOfNullCharacter(ref char searchSpace)
590581
{
591582
Debug.Assert(lengthToExamine >= Vector128<ushort>.Count);
592583

593-
Vector128<ushort> search = Vector128.LoadUnsafe(ref ushortSearchSpace, (uint)offset);
584+
Vector128<ushort> search = *(Vector128<ushort>*)(searchSpace + (nuint)offset);
594585

595586
// Same method as above
596587
Vector128<ushort> compareResult = Vector128.Equals(Vector128<ushort>.Zero, search);
@@ -630,7 +621,7 @@ public static unsafe int IndexOfNullCharacter(ref char searchSpace)
630621
{
631622
Debug.Assert(lengthToExamine >= Vector<ushort>.Count);
632623

633-
var matches = Vector.Equals(Vector<ushort>.Zero, LoadVector(ref searchSpace, offset));
624+
var matches = Vector.Equals(Vector<ushort>.Zero, *(Vector<ushort>*)(searchSpace + (nuint)offset));
634625
if (Vector<ushort>.Zero.Equals(matches))
635626
{
636627
offset += Vector<ushort>.Count;
@@ -708,28 +699,17 @@ private static nint GetCharVector256SpanLength(nint offset, nint length)
708699
=> (length - offset) & ~(Vector256<ushort>.Count - 1);
709700

710701
[MethodImpl(MethodImplOptions.AggressiveInlining)]
711-
private static unsafe nint UnalignedCountVector(ref char searchSpace)
702+
private static unsafe nint UnalignedCountVector(char* searchSpace)
712703
{
713704
const int ElementsPerByte = sizeof(ushort) / sizeof(byte);
714-
// Figure out how many characters to read sequentially until we are vector aligned
715-
// This is equivalent to:
716-
// unaligned = ((int)pCh % sizeof(Vector<ushort>) / ElementsPerByte
717-
// length = (Vector<ushort>.Count - unaligned) % Vector<ushort>.Count
718-
719-
// This alignment is only valid if the GC does not relocate; so we use ReadUnaligned to get the data.
720-
// If a GC does occur and alignment is lost, the GC cost will outweigh any gains from alignment so it
721-
// isn't too important to pin to maintain the alignment.
722-
return (nint)(uint)(-(int)Unsafe.AsPointer(ref searchSpace) / ElementsPerByte) & (Vector<ushort>.Count - 1);
705+
return (nint)(uint)(-(int)searchSpace / ElementsPerByte) & (Vector<ushort>.Count - 1);
723706
}
724707

725708
[MethodImpl(MethodImplOptions.AggressiveInlining)]
726-
private static unsafe nint UnalignedCountVector128(ref char searchSpace)
709+
private static unsafe nint UnalignedCountVector128(char* searchSpace)
727710
{
728711
const int ElementsPerByte = sizeof(ushort) / sizeof(byte);
729-
// This alignment is only valid if the GC does not relocate; so we use ReadUnaligned to get the data.
730-
// If a GC does occur and alignment is lost, the GC cost will outweigh any gains from alignment so it
731-
// isn't too important to pin to maintain the alignment.
732-
return (nint)(uint)(-(int)Unsafe.AsPointer(ref searchSpace) / ElementsPerByte) & (Vector128<ushort>.Count - 1);
712+
return (nint)(uint)(-(int)searchSpace / ElementsPerByte) & (Vector128<ushort>.Count - 1);
733713
}
734714

735715
public static void Reverse(ref char buf, nuint length)

src/libraries/System.Private.CoreLib/src/System/String.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -575,9 +575,9 @@ public StringRuneEnumerator EnumerateRunes()
575575
return new StringRuneEnumerator(this);
576576
}
577577

578-
internal static unsafe int wcslen(char* ptr) => SpanHelpers.IndexOfNullCharacter(ref *ptr);
578+
internal static unsafe int wcslen(char* ptr) => SpanHelpers.IndexOfNullCharacter(ptr);
579579

580-
internal static unsafe int strlen(byte* ptr) => SpanHelpers.IndexOfNullByte(ref *ptr);
580+
internal static unsafe int strlen(byte* ptr) => SpanHelpers.IndexOfNullByte(ptr);
581581

582582
//
583583
// IConvertible implementation

0 commit comments

Comments
 (0)