diff --git a/.vscode/launch.json b/.vscode/launch.json index 1bbf4d2a..339d2cdc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/Cesium.Compiler/bin/Debug/net6.0/Cesium.Compiler.dll", + "program": "${workspaceFolder}/Cesium.Compiler/bin/Debug/net7.0/Cesium.Compiler.dll", "args": [], "cwd": "${workspaceFolder}/Cesium.Compiler", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console @@ -23,4 +23,4 @@ "request": "attach" } ] -} \ No newline at end of file +} diff --git a/Cesium.Compiler/Compilation.cs b/Cesium.Compiler/Compilation.cs index 865db0a5..8c6d8696 100644 --- a/Cesium.Compiler/Compilation.cs +++ b/Cesium.Compiler/Compilation.cs @@ -141,7 +141,7 @@ private static void SaveAssembly( { var runtimeConfigFilePath = Path.ChangeExtension(outputFilePath, "runtimeconfig.json"); Console.WriteLine($"Generating a .NET 6 runtime config at {runtimeConfigFilePath}."); - File.WriteAllText(runtimeConfigFilePath, RuntimeConfig.EmitNet6()); + File.WriteAllText(runtimeConfigFilePath, RuntimeConfig.EmitNet7()); } } } diff --git a/Cesium.Compiler/RuntimeConfig.cs b/Cesium.Compiler/RuntimeConfig.cs index 2b75217d..f7c935ab 100644 --- a/Cesium.Compiler/RuntimeConfig.cs +++ b/Cesium.Compiler/RuntimeConfig.cs @@ -2,19 +2,6 @@ namespace Cesium.Compiler; internal static class RuntimeConfig { - public static string EmitNet6() => """ - { - "runtimeOptions": { - "tfm": "net6.0", - "rollForward": "Major", - "framework": { - "name": "Microsoft.NETCore.App", - "version": "6.0.0" - } - } - } - """.ReplaceLineEndings("\n"); - public static string EmitNet7() => """ { "runtimeOptions": { diff --git a/Cesium.Runtime.Tests/Cesium.Runtime.Tests.csproj b/Cesium.Runtime.Tests/Cesium.Runtime.Tests.csproj index ec7c60b9..df2f8dda 100644 --- a/Cesium.Runtime.Tests/Cesium.Runtime.Tests.csproj +++ b/Cesium.Runtime.Tests/Cesium.Runtime.Tests.csproj @@ -2,7 +2,7 @@ net7.0 - $(TargetFrameworks);net48 + enable false true diff --git a/Cesium.Runtime.Tests/StringFunctionTests.cs b/Cesium.Runtime.Tests/StringFunctionTests.cs new file mode 100644 index 00000000..c3d0714a --- /dev/null +++ b/Cesium.Runtime.Tests/StringFunctionTests.cs @@ -0,0 +1,75 @@ +using System.Text; + +namespace Cesium.Runtime.Tests; + +public unsafe class StringFunctionTests +{ + [Theory] + [InlineData("Hello\0", 5)] + [InlineData("Goodbye\0", 7)] + [InlineData("Hello\0Goodbye\0", 5)] + [InlineData(" \0", 18)] + public void StrLen(string input, int expected) + { + // TODO: If you are rich enough to procure a 2-4+ GB RAM runner, + // please update this test to exercise the path where the string + // length exceeds int.MaxLength of bytes. + var bytes = Encoding.UTF8.GetBytes(input); + fixed (byte* str = bytes) + { + var actual = StringFunctions.StrLen(str); + + Assert.Equal((nuint)expected, actual); + } + } + + [Fact] + public void StrLen_Null() + { + var actual = StringFunctions.StrLen(null); + + Assert.Equal((nuint)0, actual); + } + + [Theory] + [InlineData("Hello\n", 5)] + [InlineData("Goodbye\n", 7)] + [InlineData("Hello\nGoodbye\n", 5)] + [InlineData(" \n", 18)] + public void StrChr(string input, int expectedOffset) + { + var needle = '\n'; + var bytes = Encoding.UTF8.GetBytes(input); + fixed (byte* str = bytes) + { + var ptr = StringFunctions.StrChr(str, '\n'); + + Assert.Equal((byte)needle, *ptr); + Assert.Equal(expectedOffset, (int)(ptr - str)); + } + } + + [Theory] + [InlineData("Hello\0")] + [InlineData("Goodbye\0")] + [InlineData("Hello Goodbye\0")] + [InlineData(" \0")] + public void StrChr_NotFound(string input) + { + var bytes = Encoding.UTF8.GetBytes(input); + fixed (byte* str = bytes) + { + var actual = StringFunctions.StrChr(str, '\n'); + + Assert.True(actual is null); + } + } + + [Fact] + public void StrChr_Null() + { + var actual = StringFunctions.StrChr(null, '\0'); + + Assert.True(actual is null); + } +} diff --git a/Cesium.Runtime/Cesium.Runtime.csproj b/Cesium.Runtime/Cesium.Runtime.csproj index d8fe30c4..53844648 100644 --- a/Cesium.Runtime/Cesium.Runtime.csproj +++ b/Cesium.Runtime/Cesium.Runtime.csproj @@ -1,7 +1,7 @@ - netstandard2.0;net6.0 + net7.0 enable enable true diff --git a/Cesium.Runtime/StringFunctions.cs b/Cesium.Runtime/StringFunctions.cs index df755188..7510b882 100644 --- a/Cesium.Runtime/StringFunctions.cs +++ b/Cesium.Runtime/StringFunctions.cs @@ -1,9 +1,7 @@ -#if NETSTANDARD -using System.Text; -#else -using System.Collections.Specialized; -using System.Runtime.InteropServices; -#endif +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; namespace Cesium.Runtime; @@ -14,50 +12,57 @@ public static unsafe class StringFunctions { public static nuint StrLen(byte* str) { -#if NETSTANDARD - if (str == null) + if (str != null) { - return 0; - } + var start = str; + while ((nuint)str % 16 != 0) + { + if (*str is 0) + { + goto Done; + } + str++; + } - Encoding encoding = Encoding.UTF8; - int byteLength = 0; - byte* search = str; - while (*search != '\0') - { - byteLength++; - search++; + while (true) + { + var eqmask = Vector128.Equals( + Vector128.LoadAligned(str), + Vector128.Zero); + if (eqmask == Vector128.Zero) + { + str += Vector128.Count; + continue; + } + + str += IndexOfMatch(eqmask); + break; + } + Done: + return (nuint)str - (nuint)start; } - int stringLength = encoding.GetCharCount(str, byteLength); - return (uint)stringLength; -#else - return (uint)(Marshal.PtrToStringUTF8((nint)str)?.Length ?? 0); -#endif + return 0; } public static byte* StrCpy(byte* dest, byte* src) { - if (dest == null) + if (dest != null) { - return null; - } + var result = dest; + if (src != null) + { + // SIMD scan into SIMD copy (traversing the data twice) + // is much faster than a single scalar check+copy loop. + var length = StrLen(src); + Buffer.MemoryCopy(src, dest, length, length); + dest += length; + } - var result = dest; - if (src == null) - { + *dest = 0; return dest; } - byte* search = src; - while (*search != '\0') - { - *dest = *search; - search++; - dest++; - } - - *dest = 0; - return result; + return null; } public static byte* StrNCpy(byte* dest, byte* src, nuint count) { @@ -186,21 +191,50 @@ public static int StrNCmp(byte* lhs, byte* rhs, nuint count) } public static byte* StrChr(byte* str, int ch) { - if (str == null) + if (str != null) { - return null; - } + byte c = (byte)ch; - while (*str != 0) - { - if (*str == ch) + while ((nuint)str % 16 != 0) { - return str; + var curr = *str; + if (curr == c) + { + goto Done; + } + else if (curr == 0) + { + goto NotFound; + } + str++; } - str++; + var element = Vector128.Create(c); + var nullByte = Vector128.Zero; + while (true) + { + var chars = Vector128.LoadAligned(str); + var eqmask = Vector128.Equals(chars, element) | + Vector128.Equals(chars, nullByte); + if (eqmask == Vector128.Zero) + { + str += Vector128.Count; + continue; + } + + str += IndexOfMatch(eqmask); + if (*str == 0) + { + goto NotFound; + } + break; + } + + Done: + return str; } + NotFound: return null; } @@ -215,7 +249,6 @@ public static int StrCmp(byte* lhs, byte* rhs) if (*lhs > *rhs) return 1; } - if (*lhs < *rhs) return -1; if (*lhs > *rhs) return 1; return 0; @@ -253,4 +286,20 @@ public static int MemCmp(void* lhs, void* rhs, nuint count) return 0; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static uint IndexOfMatch(Vector128 eqmask) + { + if (AdvSimd.Arm64.IsSupported) + { + var res = AdvSimd + .ShiftRightLogicalNarrowingLower(eqmask.AsUInt16(), 4) + .AsUInt64() + .ToScalar(); + return (uint)BitOperations.TrailingZeroCount(res) >> 2; + } + + return (uint)BitOperations.TrailingZeroCount( + eqmask.ExtractMostSignificantBits()); + } } diff --git a/Cesium.Test.Framework/CSharpCompilationUtil.cs b/Cesium.Test.Framework/CSharpCompilationUtil.cs index 2c520bb5..60ec07e1 100644 --- a/Cesium.Test.Framework/CSharpCompilationUtil.cs +++ b/Cesium.Test.Framework/CSharpCompilationUtil.cs @@ -11,7 +11,7 @@ public static class CSharpCompilationUtil public static readonly TargetRuntimeDescriptor DefaultRuntime = TargetRuntimeDescriptor.Net60; private const string _configuration = "Debug"; private const string _targetRuntime = "net7.0"; - private const string _cesiumRuntimeLibTargetRuntime = "net6.0"; + private const string _cesiumRuntimeLibTargetRuntime = "net7.0"; private const string _projectName = "TestProject"; /// Semaphore that controls the amount of simultaneously running tests.