├── .gitattributes ├── .gitignore ├── LICENSE.MIT ├── PerformanceTypes.Tests ├── Md5Tests.cs ├── PerformanceTypes.Tests.csproj ├── ReusableStreamTests.cs ├── StopwatchTests.cs ├── StringSetTests.cs ├── UnsafeStringComparerTests.cs └── UnsafeTests.cs ├── PerformanceTypes.sln ├── PerformanceTypes ├── PerformanceTypes.csproj ├── PerformanceTypes.nuspec ├── ReusableStream.BinaryReadWrite.cs ├── ReusableStream.cs ├── StopwatchStruct.cs ├── StringHash.cs ├── StringSet.cs ├── Unsafe.cs ├── UnsafeMd5.cs └── UnsafeStringComparer.cs ├── README.md ├── appveyor.yml ├── build.cmd └── build.ps1 /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | 11 | [Dd]ebug/ 12 | [Rr]elease/ 13 | x64/ 14 | # build/ 15 | [Bb]in/ 16 | [Oo]bj/ 17 | 18 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets 19 | !packages/*/build/ 20 | 21 | # MSTest test Results 22 | [Tt]est[Rr]esult*/ 23 | [Bb]uild[Ll]og.* 24 | 25 | *_i.c 26 | *_p.c 27 | *.ilk 28 | *.meta 29 | *.obj 30 | *.pch 31 | *.pdb 32 | *.pgc 33 | *.pgd 34 | *.rsp 35 | *.sbr 36 | *.tlb 37 | *.tli 38 | *.tlh 39 | *.tmp 40 | *.tmp_proj 41 | *.log 42 | *.vspscc 43 | *.vssscc 44 | .builds 45 | *.pidb 46 | *.log 47 | *.scc 48 | 49 | # Visual C++ cache files 50 | ipch/ 51 | *.aps 52 | *.ncb 53 | *.opensdf 54 | *.sdf 55 | *.cachefile 56 | 57 | # Visual Studio profiler 58 | *.psess 59 | *.vsp 60 | *.vspx 61 | 62 | # Guidance Automation Toolkit 63 | *.gpState 64 | 65 | # ReSharper is a .NET coding add-in 66 | _ReSharper*/ 67 | *.[Rr]e[Ss]harper 68 | 69 | # TeamCity is a build add-in 70 | _TeamCity* 71 | 72 | # DotCover is a Code Coverage Tool 73 | *.dotCover 74 | 75 | # NCrunch 76 | *.ncrunch* 77 | .*crunch*.local.xml 78 | 79 | # Installshield output folder 80 | [Ee]xpress/ 81 | 82 | # DocProject is a documentation generator add-in 83 | DocProject/buildhelp/ 84 | DocProject/Help/*.HxT 85 | DocProject/Help/*.HxC 86 | DocProject/Help/*.hhc 87 | DocProject/Help/*.hhk 88 | DocProject/Help/*.hhp 89 | DocProject/Help/Html2 90 | DocProject/Help/html 91 | 92 | # Click-Once directory 93 | publish/ 94 | 95 | # Publish Web Output 96 | *.Publish.xml 97 | 98 | # NuGet Packages Directory 99 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 100 | #packages/ 101 | 102 | # Windows Azure Build Output 103 | csx 104 | *.build.csdef 105 | 106 | # Windows Store app package directory 107 | AppPackages/ 108 | 109 | # Others 110 | sql/ 111 | *.Cache 112 | ClientBin/ 113 | [Ss]tyle[Cc]op.* 114 | ~$* 115 | *~ 116 | *.dbmdl 117 | *.[Pp]ublish.xml 118 | *.pfx 119 | *.publishsettings 120 | 121 | # RIA/Silverlight projects 122 | Generated_Code/ 123 | 124 | # Backup & report files from converting an old project file to a newer 125 | # Visual Studio version. Backup files are not needed, because we have git ;-) 126 | _UpgradeReport_Files/ 127 | Backup*/ 128 | UpgradeLog*.XML 129 | UpgradeLog*.htm 130 | 131 | # SQL Server files 132 | App_Data/*.mdf 133 | App_Data/*.ldf 134 | 135 | 136 | #LightSwitch generated files 137 | GeneratedArtifacts/ 138 | _Pvt_Extensions/ 139 | ModelManifest.xml 140 | 141 | # ========================= 142 | # Windows detritus 143 | # ========================= 144 | 145 | # Windows image file caches 146 | Thumbs.db 147 | ehthumbs.db 148 | 149 | # Folder config file 150 | Desktop.ini 151 | 152 | # Recycle Bin used on file shares 153 | $RECYCLE.BIN/ 154 | 155 | # Mac desktop service store files 156 | .DS_Store 157 | 158 | # Node.js 159 | node_modules 160 | 161 | # Roslyn Temporary Solution Files 162 | *.sln.ide/ 163 | 164 | /artifacts 165 | 166 | # JetBrains IDE 167 | .idea 168 | 169 | .vs 170 | -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Bret Copeland 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /PerformanceTypes.Tests/Md5Tests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | using System.Text; 4 | using NUnit.Framework; 5 | 6 | namespace PerformanceTypes.Tests 7 | { 8 | [TestFixture] 9 | public class Md5Tests 10 | { 11 | [Test] 12 | public unsafe void DigestEquality() 13 | { 14 | var one = default(Md5Digest); 15 | var two = default(Md5Digest); 16 | 17 | Assert.IsTrue(one == two); 18 | Assert.IsFalse(one != two); 19 | 20 | var onePtr = (byte*)&one; 21 | var twoPtr = (byte*)&two; 22 | 23 | // make sure every byte is accounted for 24 | for (var i = 0; i < Md5Digest.SIZE; i++) 25 | { 26 | onePtr[i] = 1; 27 | Assert.IsFalse(one == two); 28 | Assert.IsTrue(one != two); 29 | 30 | twoPtr[i] = 1; 31 | Assert.IsTrue(one == two); 32 | Assert.IsFalse(one != two); 33 | 34 | onePtr[i] = 0; 35 | Assert.IsFalse(one == two); 36 | Assert.IsTrue(one != two); 37 | 38 | twoPtr[i] = 0; 39 | } 40 | } 41 | 42 | [Test] 43 | public unsafe void ManagedVsUnmanaged() 44 | { 45 | var rng = new Random(); 46 | var input = new byte[100]; 47 | rng.NextBytes(input); 48 | 49 | Md5Digest managed, unmanaged; 50 | 51 | UnsafeMd5.ComputeHash(input, out managed); 52 | 53 | fixed (byte* inputPtr = input) 54 | UnsafeMd5.ComputeHash(inputPtr, input.Length, &unmanaged); 55 | 56 | Assert.AreEqual(managed, unmanaged); 57 | } 58 | 59 | [Test] 60 | public unsafe void GetBytes() 61 | { 62 | var creationBytes = new byte[Md5Digest.SIZE]; 63 | var rng = new Random(); 64 | rng.NextBytes(creationBytes); 65 | 66 | fixed (byte* creationPtr = creationBytes) 67 | { 68 | var digest = (Md5Digest*)creationPtr; 69 | var output = digest->GetBytes(); 70 | 71 | Assert.AreEqual(Md5Digest.SIZE, output.Length); 72 | 73 | for (var i = 0; i < output.Length; i++) 74 | { 75 | Assert.AreEqual(creationBytes[i], output[i]); 76 | } 77 | } 78 | } 79 | 80 | [Test] 81 | public unsafe void RandomInputs() 82 | { 83 | var rng = new Random(); 84 | var md5 = MD5.Create(); 85 | 86 | var digest = default(Md5Digest); 87 | 88 | for (var byteLength = 0; byteLength < 10000; byteLength++) 89 | { 90 | for (var i = 0; i < 5; i++) 91 | { 92 | var input = new byte[byteLength]; 93 | rng.NextBytes(input); 94 | 95 | var expected = md5.ComputeHash(input); 96 | 97 | fixed (byte* ptr = input) 98 | { 99 | UnsafeMd5.ComputeHash(ptr, input.Length, &digest); 100 | } 101 | 102 | AssertDigestsAreEqual(input, expected, ref digest); 103 | } 104 | } 105 | } 106 | 107 | [Test] 108 | public unsafe void WriteBytes() 109 | { 110 | var input = new byte[100]; 111 | var rng = new Random(); 112 | rng.NextBytes(input); 113 | 114 | Md5Digest digest; 115 | UnsafeMd5.ComputeHash(input, out digest); 116 | 117 | var expected = digest.GetBytes(); 118 | 119 | var buffer1 = new byte[Md5Digest.SIZE + 2]; 120 | var buffer2 = new byte[Md5Digest.SIZE + 2]; 121 | 122 | fixed (byte* onePtr = buffer1) 123 | { 124 | digest.WriteBytes(&onePtr[1]); 125 | digest.WriteBytes(buffer2, 1); 126 | } 127 | 128 | const int end = Md5Digest.SIZE + 1; 129 | 130 | Assert.AreEqual(0, buffer1[0]); 131 | Assert.AreEqual(0, buffer2[0]); 132 | Assert.AreEqual(0, buffer1[end]); 133 | Assert.AreEqual(0, buffer2[end]); 134 | 135 | for (var i = 0; i < Md5Digest.SIZE; i++) 136 | { 137 | Assert.AreEqual(expected[i], buffer1[i + 1]); 138 | Assert.AreEqual(expected[i], buffer2[i + 1]); 139 | } 140 | } 141 | 142 | static unsafe void AssertDigestsAreEqual(byte[] input, byte[] expected, ref Md5Digest actual) 143 | { 144 | Assert.AreEqual(Md5Digest.SIZE, expected.Length); 145 | 146 | fixed (byte* expPtr = expected) 147 | { 148 | var expectedDigestPtr = (Md5Digest*)expPtr; 149 | 150 | if (*expectedDigestPtr != actual) 151 | { 152 | var msg = GetHashMismatchMessage(input, expected, actual.GetBytes()); 153 | Assert.Fail(msg); 154 | } 155 | } 156 | } 157 | 158 | static string GetHashMismatchMessage(byte[] input, byte[] expected, byte[] actual) 159 | { 160 | var sb = new StringBuilder(); 161 | 162 | sb.AppendLine("MD5 Hash Mismatch"); 163 | sb.AppendLine(); 164 | 165 | sb.Append("Expected: "); 166 | WriteHexBytes(expected, sb); 167 | sb.AppendLine(); 168 | 169 | sb.Append("Actual: "); 170 | WriteHexBytes(actual, sb); 171 | sb.AppendLine(); 172 | 173 | sb.Append("var input = "); 174 | WriteByteArray(input, sb); 175 | sb.AppendLine(";"); 176 | sb.AppendLine(); 177 | 178 | return sb.ToString(); 179 | } 180 | 181 | static void WriteHexBytes(byte[] bytes, StringBuilder sb) 182 | { 183 | foreach (var b in bytes) 184 | { 185 | sb.Append(Convert.ToString(b, 16).PadLeft(2, '0')); 186 | sb.Append(' '); 187 | } 188 | } 189 | 190 | static void WriteByteArray(byte[] bytes, StringBuilder sb) 191 | { 192 | sb.Append('{'); 193 | 194 | foreach (var b in bytes) 195 | { 196 | sb.Append(b); 197 | sb.Append(','); 198 | } 199 | 200 | sb.Append('}'); 201 | } 202 | } 203 | } -------------------------------------------------------------------------------- /PerformanceTypes.Tests/PerformanceTypes.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net462 4 | true 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /PerformanceTypes.Tests/ReusableStreamTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using NUnit.Framework; 5 | 6 | namespace PerformanceTypes.Tests 7 | { 8 | [TestFixture] 9 | public class ReusableStreamTests 10 | { 11 | [Test] 12 | public void Resetting() 13 | { 14 | var s = new ReusableStream(16); 15 | 16 | Assert.AreEqual(0, s.Position); 17 | Assert.AreEqual(0, s.Length); 18 | 19 | var written = long.MaxValue; 20 | s.Write(written); 21 | 22 | Assert.AreEqual(8, s.Position); 23 | Assert.AreEqual(8, s.Length); 24 | 25 | s.ResetForReading(); 26 | 27 | Assert.AreEqual(0, s.Position); 28 | Assert.AreEqual(8, s.Length); 29 | 30 | var read = s.ReadInt64(); 31 | 32 | Assert.AreEqual(written, read); 33 | Assert.AreEqual(8, s.Position); 34 | Assert.AreEqual(8, s.Length); 35 | 36 | s.ResetForWriting(); 37 | 38 | Assert.AreEqual(0, s.Position); 39 | Assert.AreEqual(0, s.Length); 40 | 41 | written = 1; 42 | s.Write(written); 43 | 44 | Assert.AreEqual(8, s.Position); 45 | Assert.AreEqual(8, s.Length); 46 | 47 | s.ResetForReading(); 48 | 49 | Assert.AreEqual(0, s.Position); 50 | Assert.AreEqual(8, s.Length); 51 | 52 | read = s.ReadInt64(); 53 | 54 | Assert.AreEqual(written, read); 55 | Assert.AreEqual(8, s.Position); 56 | Assert.AreEqual(8, s.Length); 57 | } 58 | 59 | [Test] 60 | public void ReadWritePrimitives() 61 | { 62 | var s = new ReusableStream(100); 63 | var rng = new Random(); 64 | 65 | var b = (byte)rng.Next(255); 66 | var sb = (sbyte)rng.Next(255); 67 | var sh = (short)rng.Next(short.MinValue, short.MaxValue); 68 | var ush = (ushort)rng.Next(ushort.MaxValue); 69 | var i = rng.Next(int.MinValue, int.MaxValue); 70 | var ui = (uint)rng.Next(int.MinValue, int.MaxValue); 71 | var l = (long)RandomULong(rng); 72 | var ul = RandomULong(rng); 73 | var f = (float)rng.NextDouble(); 74 | var d = rng.NextDouble(); 75 | var c = (char)rng.Next(char.MinValue, char.MaxValue); 76 | var t = DateTime.UtcNow; 77 | var g = Guid.NewGuid(); 78 | 79 | var expectedLength = 0; 80 | 81 | s.Write(b); 82 | expectedLength += 1; 83 | Assert.AreEqual(expectedLength, s.Length); 84 | s.Write(sb); 85 | expectedLength += 1; 86 | Assert.AreEqual(expectedLength, s.Length); 87 | s.Write(sh); 88 | expectedLength += 2; 89 | Assert.AreEqual(expectedLength, s.Length); 90 | s.Write(ush); 91 | expectedLength += 2; 92 | Assert.AreEqual(expectedLength, s.Length); 93 | s.Write(i); 94 | expectedLength += 4; 95 | Assert.AreEqual(expectedLength, s.Length); 96 | s.Write(ui); 97 | expectedLength += 4; 98 | Assert.AreEqual(expectedLength, s.Length); 99 | s.Write(l); 100 | expectedLength += 8; 101 | Assert.AreEqual(expectedLength, s.Length); 102 | s.Write(ul); 103 | expectedLength += 8; 104 | Assert.AreEqual(expectedLength, s.Length); 105 | s.Write(f); 106 | expectedLength += 4; 107 | Assert.AreEqual(expectedLength, s.Length); 108 | s.Write(d); 109 | expectedLength += 8; 110 | Assert.AreEqual(expectedLength, s.Length); 111 | s.Write(c); 112 | expectedLength += 2; 113 | Assert.AreEqual(expectedLength, s.Length); 114 | s.Write(false); 115 | expectedLength += 1; 116 | Assert.AreEqual(expectedLength, s.Length); 117 | s.Write(true); 118 | expectedLength += 1; 119 | Assert.AreEqual(expectedLength, s.Length); 120 | s.Write(t); 121 | expectedLength += 8; 122 | Assert.AreEqual(expectedLength, s.Length); 123 | s.Write(g); 124 | expectedLength += 16; 125 | Assert.AreEqual(expectedLength, s.Length); 126 | 127 | s.ResetForReading(); 128 | 129 | Assert.AreEqual(b, s.ReadUInt8()); 130 | Assert.AreEqual(sb, s.ReadInt8()); 131 | Assert.AreEqual(sh, s.ReadInt16()); 132 | Assert.AreEqual(ush, s.ReadUInt16()); 133 | Assert.AreEqual(i, s.ReadInt32()); 134 | Assert.AreEqual(ui, s.ReadUInt32()); 135 | Assert.AreEqual(l, s.ReadInt64()); 136 | Assert.AreEqual(ul, s.ReadUInt64()); 137 | Assert.AreEqual(f, s.ReadSingle()); 138 | Assert.AreEqual(d, s.ReadDouble()); 139 | Assert.AreEqual(c, s.ReadChar()); 140 | Assert.AreEqual(false, s.ReadBoolean()); 141 | Assert.AreEqual(true, s.ReadBoolean()); 142 | Assert.AreEqual(t, s.ReadDateTime()); 143 | Assert.AreEqual(g, s.ReadGuid()); 144 | 145 | // verify that we read to the end 146 | Assert.AreEqual(s.Length, s.Position); 147 | 148 | s.ResetForReading(); 149 | Assert.AreEqual((int)b, s.ReadByte()); 150 | } 151 | 152 | static ulong RandomULong(Random rng) 153 | { 154 | var low = (ulong)rng.Next(int.MinValue, int.MaxValue); 155 | var high = (ulong)rng.Next(int.MinValue, int.MaxValue); 156 | 157 | return (high << 32) | low; 158 | } 159 | 160 | [Test] 161 | public void CanGrow() 162 | { 163 | var s = new ReusableStream(8, true); 164 | 165 | Assert.AreEqual(8, s.Capacity); 166 | s.Write((long)3); 167 | 168 | // there shouldn't be any change yet 169 | Assert.AreEqual(8, s.Capacity); 170 | 171 | s.Write(4); 172 | Assert.IsTrue(s.Capacity >= 12); 173 | } 174 | 175 | [Test] 176 | public void CanGrowDisabled() 177 | { 178 | var s = new ReusableStream(8, false); 179 | 180 | Assert.AreEqual(8, s.Capacity); 181 | s.Write((long)3); 182 | 183 | // there shouldn't be any change 184 | Assert.AreEqual(8, s.Capacity); 185 | 186 | Assert.Throws(() => s.Write(4)); 187 | 188 | // there shouldn't be any change 189 | Assert.AreEqual(8, s.Capacity); 190 | } 191 | 192 | [Test] 193 | public void UninitializedStream() 194 | { 195 | var s = new ReusableStream(); 196 | 197 | Assert.Throws(() => s.Write(true)); 198 | Assert.Throws(() => s.ReadBoolean()); 199 | } 200 | 201 | [Test] 202 | public void BadInitializationParameters() 203 | { 204 | Assert.Throws(() => new ReusableStream(null, 0, 0)); 205 | Assert.Throws(() => new ReusableStream(new byte[2], -1, 0)); 206 | Assert.Throws(() => new ReusableStream(new byte[2], 3, 0)); 207 | Assert.Throws(() => new ReusableStream(new byte[2], 0, -1)); 208 | Assert.Throws(() => new ReusableStream(new byte[2], 0, 3)); 209 | Assert.Throws(() => new ReusableStream(new byte[2], 1, 2)); 210 | } 211 | 212 | [Test] 213 | public unsafe void ReadWriteBytes() 214 | { 215 | var s = new ReusableStream(4000); 216 | var rng = new Random(); 217 | 218 | var tests = new byte[][] 219 | { 220 | new byte[1], 221 | new byte[2], 222 | new byte[3], 223 | new byte[4], 224 | new byte[5], 225 | new byte[6], 226 | new byte[7], 227 | new byte[8], 228 | new byte[9], 229 | new byte[10], 230 | new byte[11], 231 | new byte[12], 232 | new byte[13], 233 | new byte[14], 234 | new byte[15], 235 | new byte[16], 236 | new byte[17], 237 | new byte[127], 238 | new byte[128], 239 | new byte[129], 240 | new byte[255], 241 | new byte[256], 242 | new byte[257], 243 | new byte[400], 244 | new byte[800], 245 | new byte[1200], 246 | }; 247 | 248 | var expectedLength = 0; 249 | 250 | foreach (var test in tests) 251 | { 252 | rng.NextBytes(test); 253 | s.Write(test); 254 | expectedLength += test.Length; 255 | Assert.AreEqual(expectedLength, s.Length); 256 | } 257 | 258 | s.ResetForReading(); 259 | 260 | foreach (var test in tests) 261 | { 262 | var read = new byte[test.Length]; 263 | s.Read(read, 0, read.Length); 264 | AssertBytesAreEqual(test, read); 265 | } 266 | 267 | // do the same thing using the unsafe methods now 268 | 269 | s = new ReusableStream(4000); 270 | 271 | expectedLength = 0; 272 | 273 | foreach (var test in tests) 274 | { 275 | fixed (byte* ptr = test) 276 | { 277 | s.Write(ptr, test.Length); 278 | } 279 | expectedLength += test.Length; 280 | Assert.AreEqual(expectedLength, s.Length); 281 | } 282 | 283 | s.ResetForReading(); 284 | 285 | foreach (var test in tests) 286 | { 287 | var read = new byte[test.Length + 16]; 288 | fixed (byte* ptr = read) 289 | { 290 | const ulong GUARD = 0xAAAAAAAAAAAAAAAA; 291 | 292 | var startGuard = (ulong*)ptr; 293 | var data = &ptr[8]; 294 | var endGuard = (ulong*)&data[test.Length]; 295 | 296 | *startGuard = GUARD; 297 | *endGuard = GUARD; 298 | 299 | s.Read(data, test.Length); 300 | 301 | Assert.AreEqual(GUARD, *startGuard); 302 | Assert.AreEqual(GUARD, *endGuard); 303 | } 304 | 305 | var actual = new byte[test.Length]; 306 | Array.Copy(read, 8, actual, 0, test.Length); 307 | 308 | AssertBytesAreEqual(test, actual); 309 | } 310 | } 311 | 312 | [Test] 313 | public unsafe void ReadPastEnd() 314 | { 315 | var s = new ReusableStream(16); 316 | 317 | s.Write((ulong)3); 318 | 319 | var buffer = new byte[16]; 320 | fixed (byte* p = buffer) 321 | { 322 | var bufferPtr = p; 323 | 324 | Assert.AreEqual(-1, s.ReadByte()); 325 | Assert.Throws(() => s.ReadBoolean()); 326 | Assert.Throws(() => s.ReadInt8()); 327 | Assert.Throws(() => s.ReadUInt8()); 328 | Assert.Throws(() => s.ReadInt16()); 329 | Assert.Throws(() => s.ReadUInt16()); 330 | Assert.Throws(() => s.ReadInt32()); 331 | Assert.Throws(() => s.ReadUInt32()); 332 | Assert.Throws(() => s.ReadInt64()); 333 | Assert.Throws(() => s.ReadUInt64()); 334 | Assert.Throws(() => s.ReadSingle()); 335 | Assert.Throws(() => s.ReadDouble()); 336 | Assert.Throws(() => s.ReadDateTime()); 337 | Assert.Throws(() => s.ReadGuid()); 338 | Assert.Throws(() => s.ReadString(true)); 339 | 340 | Assert.AreEqual(0, s.Read(buffer, 0, 1)); 341 | Assert.AreEqual(0, s.Read(bufferPtr, 1)); 342 | 343 | s.ResetForReading(); 344 | Assert.AreEqual(8, s.Read(buffer, 0, 9)); 345 | 346 | s.ResetForReading(); 347 | Assert.AreEqual(8, s.Read(bufferPtr, 9)); 348 | } 349 | } 350 | 351 | [Test] 352 | public void Seek() 353 | { 354 | var data = new byte[16]; 355 | var rng = new Random(); 356 | rng.NextBytes(data); 357 | const int offset = 2; 358 | var s = new ReusableStream(data, offset, data.Length - offset); 359 | 360 | Assert.AreEqual(data[offset], s.ReadUInt8()); 361 | 362 | s.Seek(0, SeekOrigin.Begin); 363 | Assert.AreEqual(data[offset], s.ReadUInt8()); 364 | 365 | s.Seek(2, SeekOrigin.Current); 366 | Assert.AreEqual(data[offset + 3], s.ReadUInt8()); 367 | 368 | s.Seek(-1, SeekOrigin.Current); 369 | Assert.AreEqual(data[offset + 3], s.ReadUInt8()); 370 | 371 | s.Seek(-1, SeekOrigin.End); 372 | Assert.AreEqual(data[data.Length - 1], s.ReadUInt8()); 373 | } 374 | 375 | [Test] 376 | public void ReplacingData() 377 | { 378 | var rng = new Random(); 379 | var one = new byte[16]; 380 | var two = new byte[32]; 381 | rng.NextBytes(one); 382 | rng.NextBytes(two); 383 | 384 | var readable = one.Length - 2; 385 | var s = new ReusableStream(one, 0, readable); 386 | 387 | Assert.AreEqual(readable, s.Length); 388 | Assert.AreEqual(readable, s.UnreadByteCount); 389 | Assert.AreEqual(0, s.Offset); 390 | Assert.AreEqual(0, s.Position); 391 | 392 | var readCount = 0; 393 | while (s.UnreadByteCount > 0) 394 | { 395 | Assert.AreEqual(one[readCount], s.ReadUInt8()); 396 | readCount++; 397 | } 398 | 399 | Assert.AreEqual(readable, readCount); 400 | 401 | readable = two.Length - 2; 402 | s.ReplaceData(two, 2, readable); 403 | 404 | Assert.AreEqual(readable, s.Length); 405 | Assert.AreEqual(readable, s.UnreadByteCount); 406 | Assert.AreEqual(2, s.Offset); 407 | Assert.AreEqual(0, s.Position); 408 | 409 | readCount = 0; 410 | while (s.UnreadByteCount > 0) 411 | { 412 | Assert.AreEqual(two[2 + readCount], s.ReadUInt8()); 413 | readCount++; 414 | } 415 | 416 | Assert.AreEqual(readable, readCount); 417 | } 418 | 419 | [Test] 420 | public void SetLength() 421 | { 422 | var s = new ReusableStream(16); 423 | 424 | Assert.AreEqual(0, s.Length); 425 | Assert.AreEqual(0, s.UnreadByteCount); 426 | Assert.AreEqual(16, s.Capacity); 427 | 428 | s.SetLength(4); 429 | Assert.AreEqual(4, s.Length); 430 | Assert.AreEqual(4, s.UnreadByteCount); 431 | Assert.AreEqual(16, s.Capacity); 432 | 433 | s.ReadInt32(); 434 | Assert.AreEqual(4, s.Length); 435 | Assert.AreEqual(0, s.UnreadByteCount); 436 | Assert.AreEqual(16, s.Capacity); 437 | 438 | s.SetLength(16); 439 | Assert.AreEqual(16, s.Length); 440 | Assert.AreEqual(12, s.UnreadByteCount); 441 | Assert.AreEqual(16, s.Capacity); 442 | 443 | s.SetLength(17); 444 | Assert.AreEqual(17, s.Length); 445 | Assert.AreEqual(13, s.UnreadByteCount); 446 | Assert.IsTrue(s.Capacity >= 17); 447 | 448 | s.SetLength(0); 449 | Assert.AreEqual(0, s.Length); 450 | Assert.AreEqual(0, s.UnreadByteCount); 451 | Assert.IsTrue(s.Capacity >= 17); 452 | 453 | Assert.Throws(() => s.SetLength(-1)); 454 | } 455 | 456 | [Test] 457 | public void VarInts() 458 | { 459 | var s = new ReusableStream(1000); 460 | 461 | var ints = new ulong[] 462 | { 463 | 17, 464 | 23, 465 | 0, 466 | 1, 467 | 2, 468 | 127, 469 | 128, 470 | 255, 471 | 256, 472 | (ulong)short.MaxValue, 473 | ushort.MaxValue, 474 | int.MaxValue, 475 | uint.MaxValue, 476 | long.MaxValue, 477 | ulong.MaxValue, 478 | 0x7FUL, 479 | 0x7FUL + 1, 480 | 0x3FFFUL, 481 | 0x3FFFUL + 1, 482 | 0x1FFFFFUL, 483 | 0x1FFFFFUL + 1, 484 | 0xFFFFFFFUL, 485 | 0xFFFFFFFUL + 1, 486 | 0x7FFFFFFFFUL, 487 | 0x7FFFFFFFFUL + 1, 488 | 0x3FFFFFFFFFFUL, 489 | 0x3FFFFFFFFFFUL + 1, 490 | 0x1FFFFFFFFFFFFUL, 491 | 0x1FFFFFFFFFFFFUL + 1, 492 | 0xFFFFFFFFFFFFFFUL, 493 | 0xFFFFFFFFFFFFFFUL + 1, 494 | 0x7FFFFFFFFFFFFFFFUL, 495 | 0x7FFFFFFFFFFFFFFFUL + 1, 496 | }; 497 | 498 | foreach (var ui in ints) 499 | { 500 | s.WriteVarUInt(ui); 501 | } 502 | 503 | s.ResetForReading(); 504 | 505 | foreach (var ui in ints) 506 | { 507 | var read = s.ReadVarUInt(); 508 | Assert.AreEqual(ui, read); 509 | } 510 | } 511 | 512 | [Test] 513 | public void StringEncodingLengths() 514 | { 515 | var s = new ReusableStream(100); 516 | 517 | var expectedLength = 0; 518 | 519 | var strings = new[] 520 | { 521 | "a", 522 | "瀬", 523 | "𐐷", 524 | }; 525 | 526 | var encodings = new[] 527 | { 528 | Encoding.ASCII, 529 | Encoding.UTF7, 530 | Encoding.UTF8, 531 | Encoding.Unicode, 532 | Encoding.BigEndianUnicode, 533 | Encoding.UTF32, 534 | }; 535 | 536 | foreach (var str in strings) 537 | { 538 | foreach (var e in encodings) 539 | { 540 | s.WriteString(str, false, e); 541 | expectedLength += e.GetByteCount(str) + 1; 542 | Assert.AreEqual(expectedLength, s.Length); 543 | } 544 | } 545 | 546 | s.ResetForReading(); 547 | 548 | foreach (var str in strings) 549 | { 550 | foreach (var e in encodings) 551 | { 552 | var read = s.ReadString(false, e); 553 | 554 | if (e == Encoding.ASCII) 555 | continue; 556 | 557 | Assert.AreEqual(str, read); 558 | } 559 | } 560 | } 561 | 562 | [Test] 563 | public unsafe void Strings() 564 | { 565 | // not including ASCII since it won't round-trip for the unicode strings 566 | var encodings = new[] 567 | { 568 | Encoding.UTF7, 569 | Encoding.UTF8, 570 | Encoding.Unicode, 571 | Encoding.BigEndianUnicode, 572 | Encoding.UTF32, 573 | }; 574 | 575 | var strings = new[] 576 | { 577 | "", 578 | "a", 579 | "cat", 580 | @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc odio ligula, pharetra eget eros et, blandit luctus justo. Fusce ultricies id tortor sit amet laoreet. Cras posuere tellus vel aliquam tristique. Duis et quam sit amet sapien ullamcorper blandit a sed dolor. Duis placerat nisl ac egestas suscipit. Sed lacus tellus, convallis placerat sodales vitae, tempor et urna. Duis ut leo dictum, tempus ante ut, lacinia nisi. Nunc consectetur orci nisl, vitae fringilla justo vestibulum vitae. Proin orci velit, iaculis ut ornare quis, rhoncus in lectus. 581 | 582 | Donec volutpat convallis faucibus. Donec finibus erat sit amet tortor rhoncus suscipit. Quisque facilisis lacus risus, sit amet pretium ipsum ornare et. Donec nec elementum dui. Nunc volutpat commodo metus ac tincidunt. Aenean eget lectus lacus. Donec mauris libero, accumsan id ex a, mollis rutrum lacus. Praesent mollis pretium ipsum eu faucibus. Fusce posuere congue libero, ut aliquam nulla dapibus vitae. Etiam sit amet convallis justo, sit amet hendrerit diam. 583 | 584 | Suspendisse eleifend vitae quam ac tempor. Sed fringilla tempus enim in fringilla. Fusce at leo metus. Vestibulum sollicitudin tempus odio, non posuere leo congue lobortis. Maecenas aliquam diam eget urna finibus, vitae egestas ipsum ultrices. Sed risus enim, tempor eget tortor non, malesuada auctor nulla. Aliquam egestas posuere erat, quis interdum elit efficitur sed. Suspendisse malesuada, ipsum id iaculis semper, nunc tortor congue nulla, non auctor est magna vitae ligula. Nulla sit amet orci nisl. Nulla non bibendum felis, id tempor tortor. 585 | 586 | Nam neque neque, tempus quis semper id, porttitor vitae tortor. Nulla sit amet leo eros. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Fusce varius ipsum non risus lobortis, et pretium elit malesuada. Sed luctus fermentum orci, et rutrum lacus faucibus in. Integer sodales leo aliquet mattis tristique. Nam consequat ornare lorem eget fermentum. Vestibulum a lectus sit amet nibh egestas fermentum sit amet at est. Suspendisse quis dapibus metus, sed imperdiet felis. Nulla nec consequat elit. Pellentesque pretium erat ornare felis semper tincidunt. Nullam nec magna lectus. Donec feugiat ligula urna, eget sodales ipsum fermentum sed. Morbi nec metus sit amet neque finibus hendrerit in quis ligula. Morbi lectus nulla, porta sed eros commodo, aliquam scelerisque felis. 587 | 588 | Proin fringilla pellentesque odio. Sed finibus in dolor non laoreet. Mauris in magna eget ex faucibus varius sed nec arcu. Sed tincidunt ut nulla sit amet rutrum. Mauris at neque neque. Nulla vel urna rhoncus, ultricies quam aliquet, congue nunc. Sed lectus dolor, placerat at tempor at, lacinia non ipsum. Maecenas commodo, lectus eget blandit laoreet, massa nunc egestas leo, non iaculis felis felis at lorem. Ut ut sagittis arcu, et sollicitudin nisi. Aliquam auctor porta rhoncus. Ut est enim, dictum at sollicitudin vitae, feugiat sed urna. Pellentesque pretium mi id nunc commodo, id efficitur metus vulputate. Donec rhoncus, lectus sed tincidunt accumsan, metus odio vehicula lectus, vitae tristique tellus ex id enim.", 589 | "っつ雲日御へ保瀬とれろなほニミャメ", 590 | "𐐷", 591 | "END" 592 | }; 593 | 594 | foreach (var encoding in encodings) 595 | { 596 | // try writing strings 597 | var s = new ReusableStream(10000); 598 | s.DefaultEncoding = encoding; 599 | 600 | foreach (var t in strings) 601 | { 602 | s.WriteString(t, false); 603 | } 604 | 605 | s.ResetForReading(); 606 | 607 | foreach (var t in strings) 608 | { 609 | var str = s.ReadString(false); 610 | Assert.AreEqual(t, str); 611 | } 612 | 613 | // try writing char[] 614 | s = new ReusableStream(10000); 615 | s.DefaultEncoding = encoding; 616 | 617 | foreach (var t in strings) 618 | { 619 | s.WriteString(t.ToCharArray(), 0, t.Length, false); 620 | } 621 | 622 | s.ResetForReading(); 623 | 624 | foreach (var t in strings) 625 | { 626 | var str = s.ReadString(false); 627 | Assert.AreEqual(t, str); 628 | } 629 | 630 | // try writing char* 631 | s = new ReusableStream(10000); 632 | s.DefaultEncoding = encoding; 633 | 634 | foreach (var t in strings) 635 | { 636 | fixed (char* ptr = t) 637 | { 638 | s.WriteString(ptr, t.Length, false); 639 | } 640 | } 641 | 642 | s.ResetForReading(); 643 | 644 | foreach (var t in strings) 645 | { 646 | var str = s.ReadString(false); 647 | Assert.AreEqual(t, str); 648 | } 649 | } 650 | } 651 | 652 | [Test] 653 | public void StringEdgeCases() 654 | { 655 | // The purpose of this method is to trigger some edge cases in the string writing code where the max encoded size of given string length is greater 656 | // than the remaining bytes in the stream; however, the *actual* encoded string might still fit. If the real encoded size will fit, the stream 657 | // should not grow. 658 | 659 | var s = new ReusableStream(8); 660 | 661 | var sevenAscii = "seven c"; // should fit 662 | var sevenUnicode = "seven 瀬"; // won't fit 663 | 664 | s.WriteString(sevenAscii, false); 665 | Assert.AreEqual(8, s.Length); 666 | Assert.AreEqual(8, s.Capacity); 667 | 668 | s.ResetForWriting(); 669 | 670 | s.WriteString(sevenUnicode, false); 671 | Assert.AreEqual(10, s.Length); 672 | Assert.IsTrue(s.Capacity >= 10); 673 | } 674 | 675 | [Test] 676 | public unsafe void StringsNullable() 677 | { 678 | var s = new ReusableStream(40); 679 | 680 | var expectedLength = 0; 681 | const string emptyString = ""; 682 | 683 | s.WriteString(null, true); 684 | expectedLength += 2; 685 | Assert.AreEqual(expectedLength, s.Length); 686 | 687 | s.WriteString(null, 0, 0, true); 688 | expectedLength += 2; 689 | Assert.AreEqual(expectedLength, s.Length); 690 | 691 | s.WriteString(null, 0, true); 692 | expectedLength += 2; 693 | Assert.AreEqual(expectedLength, s.Length); 694 | 695 | s.WriteString(emptyString, true); 696 | expectedLength += 2; 697 | Assert.AreEqual(expectedLength, s.Length); 698 | 699 | s.WriteString(Array.Empty(), 0, 0, true); 700 | expectedLength += 2; 701 | Assert.AreEqual(expectedLength, s.Length); 702 | 703 | fixed (char* ptr = emptyString) 704 | s.WriteString(ptr, 0, true); 705 | expectedLength += 2; 706 | Assert.AreEqual(expectedLength, s.Length); 707 | 708 | s.ResetForReading(); 709 | 710 | Assert.AreEqual(null, s.ReadString(true)); 711 | Assert.AreEqual(null, s.ReadString(true)); 712 | Assert.AreEqual(null, s.ReadString(true)); 713 | Assert.AreEqual(emptyString, s.ReadString(true)); 714 | Assert.AreEqual(emptyString, s.ReadString(true)); 715 | Assert.AreEqual(emptyString, s.ReadString(true)); 716 | 717 | Assert.Throws(() => s.WriteString(null, false)); 718 | Assert.Throws(() => s.WriteString(null, 0, 0, false)); 719 | Assert.Throws(() => s.WriteString(null, 0, false)); 720 | } 721 | 722 | [Test] 723 | public void StringInterning() 724 | { 725 | var s = new ReusableStream(100); 726 | 727 | var strings = new[] {"cat", "deer", "snail", "dog", "frog", "human"}; 728 | const int max = 4; 729 | const string exclude = "frog"; 730 | 731 | foreach (var str in strings) 732 | { 733 | s.WriteString(str, false); 734 | } 735 | 736 | s.ResetForReading(); 737 | 738 | // we're not interning yet - make sure new strings were returned 739 | foreach (var str in strings) 740 | { 741 | var read = s.ReadString(false); 742 | Assert.AreEqual(str, read); 743 | Assert.AreNotSame(str, read); 744 | } 745 | 746 | s.ResetForReading(); 747 | 748 | var options = new StringSetOptions(); 749 | options.MaxEncodedSizeToLookupInSet = max; 750 | 751 | Assert.Throws(() => s.SetDefaultStringSetOptions(options)); 752 | 753 | var set = new StringSet(10); 754 | s.StringSet = set; 755 | s.SetDefaultStringSetOptions(options); 756 | s.StringSet = null; 757 | 758 | // should throw because no StringSet has been provided 759 | Assert.Throws(() => s.ReadString(false)); 760 | 761 | foreach (var str in strings) 762 | { 763 | if (str != exclude) 764 | set.Add(str); 765 | } 766 | 767 | s.StringSet = set; 768 | 769 | // read with interning (but no auto-interning) 770 | foreach (var str in strings) 771 | { 772 | var read = s.ReadString(false); 773 | Assert.AreEqual(str, read); 774 | 775 | if (str.Length <= max && str != exclude) 776 | Assert.AreSame(str, read); 777 | else 778 | Assert.AreNotSame(str, read); 779 | } 780 | 781 | // make sure the excluded string didn't get added to the set 782 | Assert.AreEqual(null, set.GetExistingString(exclude.ToCharArray(), 0, exclude.Length)); 783 | 784 | s.ResetForReading(); 785 | options.PerformDangerousAutoAddToSet = true; 786 | s.SetDefaultStringSetOptions(options); 787 | 788 | // read with auto-interning 789 | foreach (var str in strings) 790 | { 791 | var read = s.ReadString(false); 792 | Assert.AreEqual(str, read); 793 | 794 | if (str.Length <= max && str != exclude) 795 | Assert.AreSame(str, read); 796 | else 797 | Assert.AreNotSame(str, read); 798 | } 799 | 800 | // make sure the excluded string got added to the set 801 | Assert.AreEqual(exclude, set.GetExistingString(exclude.ToCharArray(), 0, exclude.Length)); 802 | } 803 | 804 | static void AssertBytesAreEqual(byte[] expected, byte[] actual) 805 | { 806 | if (expected.Length != actual.Length) 807 | throw BytesNotEqual(actual, expected); 808 | 809 | for (var i = 0; i < expected.Length; i++) 810 | { 811 | if (expected[i] != actual[i]) 812 | throw BytesNotEqual(expected, actual); 813 | } 814 | } 815 | 816 | static Exception BytesNotEqual(byte[] expected, byte[] actual) 817 | { 818 | var sb = new StringBuilder(); 819 | 820 | sb.AppendLine("Byte arrays are not equal."); 821 | sb.AppendLine(); 822 | sb.Append("Expected: "); 823 | WriteHexBytes(expected, sb); 824 | sb.AppendLine(); 825 | sb.Append("Actual: "); 826 | WriteHexBytes(actual, sb); 827 | sb.AppendLine(); 828 | 829 | return new Exception(sb.ToString()); 830 | } 831 | 832 | static void WriteHexBytes(byte[] bytes, StringBuilder sb) 833 | { 834 | foreach (var b in bytes) 835 | { 836 | sb.Append(Convert.ToString(b, 16).PadLeft(2, '0')); 837 | sb.Append(' '); 838 | } 839 | } 840 | } 841 | } -------------------------------------------------------------------------------- /PerformanceTypes.Tests/StopwatchTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using NUnit.Framework; 5 | 6 | namespace PerformanceTypes.Tests 7 | { 8 | [TestFixture] 9 | public class StopwatchTests 10 | { 11 | [Test] 12 | public void ResultMatchesStopwatch() 13 | { 14 | var sw = Stopwatch.StartNew(); 15 | var ss = new StopwatchStruct(); 16 | ss.Start(); 17 | 18 | Thread.Sleep(500); 19 | 20 | sw.Stop(); 21 | ss.Stop(); 22 | 23 | var swMs = sw.Elapsed.TotalMilliseconds; 24 | var ssMs = ss.GetElapsedMilliseconds(); 25 | 26 | var difference = Math.Abs(swMs - ssMs); 27 | 28 | Assert.IsTrue(difference < 1); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /PerformanceTypes.Tests/StringSetTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace PerformanceTypes.Tests 4 | { 5 | [TestFixture] 6 | public class StringSetTests 7 | { 8 | [Test] 9 | public void StringEquals() 10 | { 11 | const string original = "thisthatthen"; 12 | var arr = original.ToCharArray(); 13 | 14 | var set = new StringSet(10); 15 | 16 | string str; 17 | set.Add(arr, 0, 4, out str); 18 | Assert.AreEqual("this", str); 19 | 20 | set.Add(arr, 4, 4, out str); 21 | Assert.AreEqual("that", str); 22 | 23 | set.Add(arr, 8, 4, out str); 24 | Assert.AreEqual("then", str); 25 | } 26 | 27 | [Test] 28 | public void ReferenceEquals() 29 | { 30 | const string original = "thisthatthen"; 31 | var arr = original.ToCharArray(); 32 | 33 | var set = new StringSet(10); 34 | 35 | string this1, that1, then1; 36 | set.Add(arr, 0, 4, out this1); 37 | set.Add(arr, 4, 4, out that1); 38 | set.Add(arr, 8, 4, out then1); 39 | 40 | string this2, that2, then2; 41 | set.Add(arr, 0, 4, out this2); 42 | set.Add(arr, 4, 4, out that2); 43 | set.Add(arr, 8, 4, out then2); 44 | 45 | Assert.AreEqual(this1, this2); 46 | Assert.AreEqual(that1, that2); 47 | Assert.AreEqual(then1, then2); 48 | 49 | Assert.AreSame(this1, this2); 50 | Assert.AreSame(that1, that2); 51 | Assert.AreSame(then1, then2); 52 | } 53 | 54 | [Test] 55 | public void Grow() 56 | { 57 | // 0 3 6 11 15 19 22 27 32 36 58 | const string original = "onetwothreefourfivesixseveneightnineten"; 59 | var arr = original.ToCharArray(); 60 | 61 | var set = new StringSet(4); 62 | 63 | string str; 64 | set.Add(arr, 0, 3, out str); // one 65 | set.Add(arr, 3, 3, out str); // two 66 | set.Add(arr, 6, 5, out str); // three 67 | set.Add(arr, 11, 4, out str); // four 68 | Assert.AreEqual(4, set.MaxSize); 69 | 70 | // make sure we can find something 71 | Assert.AreEqual("two", set.GetExistingString(arr, 3, 3)); 72 | 73 | set.Add(arr, 15, 4, out str); // five 74 | Assert.Greater(set.MaxSize, 4, "The set should have expanded to greater than 4 maximum size."); 75 | 76 | set.Add(arr, 19, 3, out str); // six 77 | set.Add(arr, 22, 5, out str); // seven 78 | set.Add(arr, 27, 5, out str); // eight 79 | set.Add(arr, 32, 4, out str); // nine 80 | set.Add(arr, 36, 3, out str); // ten 81 | 82 | Assert.AreEqual(10, set.Count); 83 | Assert.GreaterOrEqual(set.MaxSize, set.Count); 84 | 85 | Assert.AreEqual("two", set.GetExistingString(arr, 3, 3)); 86 | Assert.AreEqual("seven", set.GetExistingString(arr, 22, 5)); 87 | } 88 | 89 | [Test] 90 | public void GetExisting() 91 | { 92 | const string original = "thisthatthen"; 93 | var arr = original.ToCharArray(); 94 | 95 | var set = new StringSet(10); 96 | 97 | Assert.IsNull(set.GetExistingString(arr, 0, 4)); 98 | 99 | string str; 100 | Assert.True(set.Add(arr, 0, 4, out str)); 101 | Assert.False(set.Add(arr, 0, 4, out str)); 102 | Assert.AreEqual(str, set.GetExistingString(arr, 0, 4)); 103 | 104 | Assert.IsNull(set.GetExistingString(arr, 4, 4)); 105 | } 106 | 107 | [Test] 108 | public void SearchCursor() 109 | { 110 | var one = "one"; 111 | var two = "two"; 112 | var three = "three"; 113 | 114 | var set = new StringSet(4); 115 | 116 | // pretend that we have some hash collisions 117 | var hash = StringHash.GetHash(one); 118 | 119 | set.Add(one, hash); 120 | set.Add(two, hash); 121 | set.Add(three, hash); 122 | 123 | var cursor = set.GetSearchCursor(hash); 124 | 125 | // add one more with the same hash to make sure the cursor doesn't change 126 | set.Add("four", hash); 127 | 128 | Assert.AreEqual(4, set.Count); 129 | Assert.AreEqual(4, set.MaxSize); // hash collisions shouldn't cause the set to grow 130 | 131 | Assert.True(cursor.MightHaveMore); 132 | Assert.AreSame(three, cursor.NextString()); 133 | Assert.True(cursor.MightHaveMore); 134 | Assert.AreSame(two, cursor.NextString()); 135 | Assert.True(cursor.MightHaveMore); 136 | Assert.AreSame(one, cursor.NextString()); 137 | Assert.False(cursor.MightHaveMore); 138 | } 139 | } 140 | } -------------------------------------------------------------------------------- /PerformanceTypes.Tests/UnsafeStringComparerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using NUnit.Framework; 4 | 5 | namespace PerformanceTypes.Tests 6 | { 7 | public class UnsafeStringComparerTests 8 | { 9 | class Sample 10 | { 11 | /// 12 | /// A randomly generated string. 13 | /// 14 | public string StringA { get; } 15 | /// 16 | /// The char array version of StringA. 17 | /// 18 | public char[] CharsA { get; } 19 | /// 20 | /// A randomly generated string, guaranteed to be not equal to StringA. 21 | /// 22 | public string StringB { get; } 23 | /// 24 | /// The char array version of StringB. 25 | /// 26 | public char[] CharsB { get; } 27 | /// 28 | /// StringC is identical to StringA, except for the final character. 29 | /// 30 | public string StringC { get; } 31 | /// 32 | /// The char array version of StringC. 33 | /// 34 | public char[] CharsC { get; } 35 | 36 | public Sample(string a, string b) 37 | { 38 | StringA = a; 39 | CharsA = a.ToCharArray(); 40 | StringB = b; 41 | CharsB = b.ToCharArray(); 42 | 43 | CharsC = a.ToCharArray(); 44 | CharsC[a.Length - 1] = (char)(CharsC[a.Length - 1] - 1); 45 | StringC = new string(CharsC); 46 | } 47 | } 48 | 49 | /// 50 | /// Each length will have three strings. The first two are completely random. The third is identical to the first except for the last character. 51 | /// 52 | static Dictionary s_samplesByLength; 53 | 54 | [OneTimeSetUp] 55 | public void Setup() 56 | { 57 | var rng = new Random(); 58 | s_samplesByLength = new Dictionary(); 59 | 60 | // add every length between 1 and 17 61 | for (var i = 1; i < 18; i++) 62 | { 63 | AddSampleStrings(i, rng); 64 | } 65 | 66 | // add values near powers of 2 up to about 10000 67 | for (var i = 32; i < 10000; i *= 2) 68 | { 69 | AddSampleStrings(i - 3, rng); 70 | AddSampleStrings(i - 2, rng); 71 | AddSampleStrings(i - 1, rng); 72 | AddSampleStrings(i - 0, rng); 73 | AddSampleStrings(i + 1, rng); 74 | } 75 | } 76 | 77 | static void AddSampleStrings(int len, Random rng) 78 | { 79 | var a = GenerateString(len, rng); 80 | 81 | string b; 82 | do { b = GenerateString(len, rng); } 83 | while (len > 0 && a == b); // make sure a and b are actually different. 84 | 85 | s_samplesByLength[len] = new Sample(a, b); 86 | } 87 | 88 | static string GenerateString(int len, Random rng) 89 | { 90 | var chars = new char[len]; 91 | 92 | for (var i = 0; i < len; i++) 93 | { 94 | chars[i] = (char)rng.Next(40, char.MaxValue); 95 | } 96 | 97 | return new string(chars); 98 | } 99 | 100 | [Test] 101 | public void StringToCharArray() 102 | { 103 | // length checks 104 | Assert.True(UnsafeStringComparer.AreEqual("", new char[0])); 105 | Assert.False(UnsafeStringComparer.AreEqual("1", new char[0])); 106 | Assert.False(UnsafeStringComparer.AreEqual("", new char[1])); 107 | Assert.False(UnsafeStringComparer.AreEqual("1", new char[2])); 108 | Assert.False(UnsafeStringComparer.AreEqual("11", new char[1])); 109 | 110 | // invalid length for char buffer (outside bounds of buffer) 111 | Assert.False(UnsafeStringComparer.AreEqual("123", "123".ToCharArray(), 1, 3)); 112 | 113 | // starting at an offset greater than zero 114 | Assert.True(UnsafeStringComparer.AreEqual("123", "0123".ToCharArray(), 1, 3)); 115 | 116 | foreach (var sample in s_samplesByLength.Values) 117 | { 118 | AssertEqual(sample.StringA, sample.CharsA); 119 | AssertEqual(sample.StringB, sample.CharsB); 120 | AssertEqual(sample.StringC, sample.CharsC); 121 | 122 | AssertNotEqual(sample.StringA, sample.CharsB); 123 | AssertNotEqual(sample.StringA, sample.CharsC); 124 | AssertNotEqual(sample.StringB, sample.CharsA); 125 | AssertNotEqual(sample.StringB, sample.CharsC); 126 | AssertNotEqual(sample.StringC, sample.CharsA); 127 | AssertNotEqual(sample.StringC, sample.CharsB); 128 | } 129 | } 130 | 131 | static void AssertEqual(string s, char[] chars) 132 | { 133 | Assert.True(UnsafeStringComparer.AreEqual(s, chars), $"Didn't match. String: \"{s}\", Chars: \"{new string(chars)}\""); 134 | } 135 | 136 | static void AssertNotEqual(string s, char[] chars) 137 | { 138 | Assert.False(UnsafeStringComparer.AreEqual(s, chars), $"Incorrect match. String: \"{s}\", Chars: \"{new string(chars)}\""); 139 | } 140 | 141 | [Test] 142 | public void StringToCharPointer() 143 | { 144 | foreach (var sample in s_samplesByLength.Values) 145 | { 146 | AssertEqualUnsafe(sample.StringA, sample.CharsA); 147 | AssertEqualUnsafe(sample.StringB, sample.CharsB); 148 | AssertEqualUnsafe(sample.StringC, sample.CharsC); 149 | 150 | AssertNotEqualUnsafe(sample.StringA, sample.CharsB); 151 | AssertNotEqualUnsafe(sample.StringA, sample.CharsC); 152 | AssertNotEqualUnsafe(sample.StringB, sample.CharsA); 153 | AssertNotEqualUnsafe(sample.StringB, sample.CharsC); 154 | AssertNotEqualUnsafe(sample.StringC, sample.CharsA); 155 | AssertNotEqualUnsafe(sample.StringC, sample.CharsB); 156 | } 157 | } 158 | 159 | static unsafe void AssertEqualUnsafe(string s, char[] chars) 160 | { 161 | fixed (char* ptr = chars) 162 | { 163 | Assert.True(UnsafeStringComparer.AreEqual(s, ptr, s.Length), $"Didn't match. String: \"{s}\", Chars: \"{new string(chars)}\""); 164 | } 165 | } 166 | 167 | static unsafe void AssertNotEqualUnsafe(string s, char[] chars) 168 | { 169 | fixed (char* ptr = chars) 170 | { 171 | Assert.False(UnsafeStringComparer.AreEqual(s, ptr, s.Length), $"Incorrect match. String: \"{s}\", Chars: \"{new string(chars)}\""); 172 | } 173 | } 174 | 175 | [Test] 176 | public void CharPointerToCharPointer() 177 | { 178 | foreach (var sample in s_samplesByLength.Values) 179 | { 180 | AssertEqualUnsafe(sample.CharsA, sample.CharsA); 181 | AssertEqualUnsafe(sample.CharsB, sample.CharsB); 182 | AssertEqualUnsafe(sample.CharsC, sample.CharsC); 183 | 184 | AssertNotEqualUnsafe(sample.CharsA, sample.CharsB); 185 | AssertNotEqualUnsafe(sample.CharsA, sample.CharsC); 186 | AssertNotEqualUnsafe(sample.CharsB, sample.CharsA); 187 | AssertNotEqualUnsafe(sample.CharsB, sample.CharsC); 188 | AssertNotEqualUnsafe(sample.CharsC, sample.CharsA); 189 | AssertNotEqualUnsafe(sample.CharsC, sample.CharsB); 190 | } 191 | } 192 | 193 | static unsafe void AssertEqualUnsafe(char[] a, char[] b) 194 | { 195 | fixed (char* aPtr = a) 196 | fixed (char* bPtr = b) 197 | { 198 | Assert.True(UnsafeStringComparer.AreEqual(aPtr, bPtr, a.Length), $"Didn't match. String: \"{new string(a)}\", Chars: \"{new string(b)}\""); 199 | } 200 | } 201 | 202 | static unsafe void AssertNotEqualUnsafe(char[] a, char[] b) 203 | { 204 | fixed (char* aPtr = a) 205 | fixed (char* bPtr = b) 206 | { 207 | Assert.False(UnsafeStringComparer.AreEqual(aPtr, bPtr, a.Length), $"Incorrect match. String: \"{new string(a)}\", Chars: \"{new string(b)}\""); 208 | } 209 | } 210 | } 211 | } -------------------------------------------------------------------------------- /PerformanceTypes.Tests/UnsafeTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using NUnit.Framework; 4 | 5 | namespace PerformanceTypes.Tests 6 | { 7 | [TestFixture] 8 | public class UnsafeTests 9 | { 10 | [Test] 11 | public unsafe void ToHexString() 12 | { 13 | var rng = new Random(); 14 | for (var byteLength = 0; byteLength < 1500; byteLength++) 15 | { 16 | for (var i = 0; i < 5; i++) 17 | { 18 | var bytes = new byte[byteLength]; 19 | rng.NextBytes(bytes); 20 | 21 | fixed (byte* ptr = bytes) 22 | { 23 | var expected = ManagedToHexString(bytes); 24 | var actual = Unsafe.ToHexString(ptr, bytes.Length); 25 | 26 | Assert.AreEqual(expected, actual); 27 | } 28 | } 29 | } 30 | } 31 | 32 | static string ManagedToHexString(byte[] bytes) 33 | { 34 | var sb = new StringBuilder(); 35 | 36 | foreach (var b in bytes) 37 | { 38 | sb.Append(Convert.ToString(b, 16).PadLeft(2, '0')); 39 | } 40 | 41 | return sb.ToString(); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /PerformanceTypes.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.25428.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PerformanceTypes", "PerformanceTypes\PerformanceTypes.csproj", "{9EB7644F-BED8-4C58-9874-29675548950C}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PerformanceTypes.Tests", "PerformanceTypes.Tests\PerformanceTypes.Tests.csproj", "{99761C8C-FD92-4047-82BC-BDD5B7E40135}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{14A12A26-4D7F-441C-9621-6FF9769F169D}" 11 | ProjectSection(SolutionItems) = preProject 12 | appveyor.yml = appveyor.yml 13 | build.cmd = build.cmd 14 | build.ps1 = build.ps1 15 | EndProjectSection 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Release|Any CPU = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {9EB7644F-BED8-4C58-9874-29675548950C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {9EB7644F-BED8-4C58-9874-29675548950C}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {9EB7644F-BED8-4C58-9874-29675548950C}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {9EB7644F-BED8-4C58-9874-29675548950C}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {99761C8C-FD92-4047-82BC-BDD5B7E40135}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {99761C8C-FD92-4047-82BC-BDD5B7E40135}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {99761C8C-FD92-4047-82BC-BDD5B7E40135}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {99761C8C-FD92-4047-82BC-BDD5B7E40135}.Release|Any CPU.Build.0 = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(SolutionProperties) = preSolution 33 | HideSolutionNode = FALSE 34 | EndGlobalSection 35 | GlobalSection(ExtensibilityGlobals) = postSolution 36 | SolutionGuid = {55F6EE79-1552-427A-BC31-115525A591A3} 37 | EndGlobalSection 38 | EndGlobal 39 | -------------------------------------------------------------------------------- /PerformanceTypes/PerformanceTypes.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | true 5 | Bret Copeland 6 | 7 | 8 | https://github.com/bretcope/PerformanceTypes 9 | 10 | https://github.com/bretcope/PerformanceTypes.git 11 | git 12 | Performance Allocations Stopwatch 13 | 2.1.0 14 | false 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /PerformanceTypes/PerformanceTypes.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $id$ 5 | $version$ 6 | PerformanceTypes 7 | Bret Copeland 8 | Bret Copeland 9 | https://github.com/bretcope/PerformanceTypes 10 | false 11 | $description$ 12 | 13 | Copyright 2015 14 | Performance Allocations Stopwatch 15 | 16 | 17 | -------------------------------------------------------------------------------- /PerformanceTypes/ReusableStream.BinaryReadWrite.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace PerformanceTypes 5 | { 6 | public partial class ReusableStream 7 | { 8 | /// 9 | /// Reads all bytes from and writes them to the stream. 10 | /// 11 | /// The buffer to read from. 12 | public void Write(byte[] buffer) 13 | { 14 | Write(buffer, 0, buffer.Length); 15 | } 16 | 17 | /// 18 | /// Writes a boolean value as a single byte with the value 0 (false) or 1 (true). 19 | /// 20 | public void Write(bool value) 21 | { 22 | WriteByte((byte)(value ? 1 : 0)); 23 | } 24 | 25 | /// 26 | /// Reads a single byte and treats it as a boolean value. 1 is interpreted as true. All other values are interpreted as false. 27 | /// 28 | public bool ReadBoolean() 29 | { 30 | return ReadUInt8() == 1; 31 | } 32 | 33 | /// 34 | /// Writes a char to the stream (two bytes). Uses the endianness of the current architecture. 35 | /// 36 | public unsafe void Write(char value) 37 | { 38 | WriteTwoBytes((byte*)&value); 39 | } 40 | 41 | /// 42 | /// Reads a char from the stream (two bytes). Uses the endianness of the current architecture. 43 | /// 44 | public unsafe char ReadChar() 45 | { 46 | char ch; 47 | ReadTwoBytes((byte*)&ch); 48 | return ch; 49 | } 50 | 51 | /// 52 | /// Writes a signed byte to the stream. 53 | /// 54 | public void Write(sbyte value) 55 | { 56 | WriteByte((byte)value); 57 | } 58 | 59 | /// 60 | /// Reads a signed byte from the stream. 61 | /// 62 | public sbyte ReadInt8() 63 | { 64 | return (sbyte)ReadOneByte(); 65 | } 66 | 67 | /// 68 | /// Writes a byte to the stream. 69 | /// 70 | public void Write(byte value) 71 | { 72 | WriteByte(value); 73 | } 74 | 75 | /// 76 | /// Reads a byte from the stream. 77 | /// 78 | public byte ReadUInt8() 79 | { 80 | return ReadOneByte(); 81 | } 82 | 83 | /// 84 | /// Writes a short (two bytes) to the stream. Uses the endianness of the current architecture. 85 | /// 86 | public unsafe void Write(short value) 87 | { 88 | WriteTwoBytes((byte*)&value); 89 | } 90 | 91 | /// 92 | /// Reads a short (two bytes) from the stream. Uses the endianness of the current architecture. 93 | /// 94 | public unsafe short ReadInt16() 95 | { 96 | short value; 97 | ReadTwoBytes((byte*)&value); 98 | return value; 99 | } 100 | 101 | /// 102 | /// Writes an unsigned short (two bytes) to the stream. Uses the endianness of the current architecture. 103 | /// 104 | public unsafe void Write(ushort value) 105 | { 106 | WriteTwoBytes((byte*)&value); 107 | } 108 | 109 | /// 110 | /// Reads an unsigned short (two bytes) from the stream. Uses the endianness of the current architecture. 111 | /// 112 | public unsafe ushort ReadUInt16() 113 | { 114 | ushort value; 115 | ReadTwoBytes((byte*)&value); 116 | return value; 117 | } 118 | 119 | /// 120 | /// Writes an int (four bytes) to the stream. Uses the endianness of the current architecture. 121 | /// 122 | public unsafe void Write(int value) 123 | { 124 | WriteFourBytes((byte*)&value); 125 | } 126 | 127 | /// 128 | /// Reads an int (four bytes) from the stream. Uses the endianness of the current architecture. 129 | /// 130 | public unsafe int ReadInt32() 131 | { 132 | int value; 133 | ReadFourBytes((byte*)&value); 134 | return value; 135 | } 136 | 137 | /// 138 | /// Writes an unsigned int (four bytes) to the stream. Uses the endianness of the current architecture. 139 | /// 140 | public unsafe void Write(uint value) 141 | { 142 | WriteFourBytes((byte*)&value); 143 | } 144 | 145 | /// 146 | /// Reads an unsigned int (four bytes) from the stream. Uses the endianness of the current architecture. 147 | /// 148 | public unsafe uint ReadUInt32() 149 | { 150 | uint value; 151 | ReadFourBytes((byte*)&value); 152 | return value; 153 | } 154 | 155 | /// 156 | /// Writes a long (eight bytes) to the stream. Uses the endianness of the current architecture. 157 | /// 158 | public unsafe void Write(long value) 159 | { 160 | WriteEightBytes((byte*)&value); 161 | } 162 | 163 | /// 164 | /// Reads a long (eight bytes) from the stream. Uses the endianness of the current architecture. 165 | /// 166 | public unsafe long ReadInt64() 167 | { 168 | long value; 169 | ReadEightBytes((byte*)&value); 170 | return value; 171 | } 172 | 173 | /// 174 | /// Writes an unsigned long (eight bytes) to the stream. Uses the endianness of the current architecture. 175 | /// 176 | public unsafe void Write(ulong value) 177 | { 178 | WriteEightBytes((byte*)&value); 179 | } 180 | 181 | /// 182 | /// Reads an unsigned long (eight bytes) from the stream. Uses the endianness of the current architecture. 183 | /// 184 | public unsafe ulong ReadUInt64() 185 | { 186 | ulong value; 187 | ReadEightBytes((byte*)&value); 188 | return value; 189 | } 190 | 191 | /// 192 | /// Writes a float (four bytes) to the stream. Uses the endianness of the current architecture. 193 | /// 194 | public unsafe void Write(float value) 195 | { 196 | WriteFourBytes((byte*)&value); 197 | } 198 | 199 | /// 200 | /// Reads a float (four bytes) from the stream. Uses the endianness of the current architecture. 201 | /// 202 | public unsafe float ReadSingle() 203 | { 204 | float value; 205 | ReadFourBytes((byte*)&value); 206 | return value; 207 | } 208 | 209 | /// 210 | /// Writes a double (eight bytes) to the stream. Uses the endianness of the current architecture. 211 | /// 212 | public unsafe void Write(double value) 213 | { 214 | WriteEightBytes((byte*)&value); 215 | } 216 | 217 | /// 218 | /// Reads a double (eight bytes) from the stream. Uses the endianness of the current architecture. 219 | /// 220 | public unsafe double ReadDouble() 221 | { 222 | double value; 223 | ReadEightBytes((byte*)&value); 224 | return value; 225 | } 226 | 227 | /// 228 | /// Calls ToBinary() on the value and writes the returned long (eight bytes). Uses the endianness of the current architecture. 229 | /// 230 | /// 231 | public unsafe void Write(DateTime value) 232 | { 233 | var int64 = value.ToBinary(); 234 | WriteEightBytes((byte*)&int64); 235 | } 236 | 237 | /// 238 | /// Reads eight bytes from the stream and generates a DateTime by calling DateTime.FromBinary. Uses the endianness of the current architecture. 239 | /// 240 | /// 241 | public unsafe DateTime ReadDateTime() 242 | { 243 | long int64; 244 | ReadEightBytes((byte*)&int64); 245 | return DateTime.FromBinary(int64); 246 | } 247 | 248 | /// 249 | /// Writes a Guid to the stream based on the raw bytes of the struct. Uses the endianness of the current architecture. 250 | /// 251 | public unsafe void Write(Guid value) 252 | { 253 | Write((byte*)&value, sizeof(Guid)); 254 | } 255 | 256 | /// 257 | /// Reads 16 bytes from the stream and treats them as a Guid struct. Uses the endianness of the current architecture. 258 | /// 259 | public unsafe Guid ReadGuid() 260 | { 261 | if (UnreadByteCount < sizeof(Guid)) 262 | throw new IndexOutOfRangeException(); 263 | 264 | Guid guid; 265 | Read((byte*)&guid, sizeof(Guid)); 266 | return guid; 267 | } 268 | 269 | /// 270 | /// Reads a length-prefixed string from the stream. 271 | /// 272 | /// True if the string can be null. 273 | /// The encoding to use. If null, the ReusableStream's will be used. 274 | /// The string interning options to use. If null, the ReusableStream's default options will be used. 275 | /// The string read from the stream. 276 | public unsafe string ReadString(bool nullable, Encoding encoding = null, StringSetOptions? setOptions = null) 277 | { 278 | var stringSetOptions = setOptions ?? _defaultStringSetOptions; 279 | var set = StringSet; 280 | if (stringSetOptions.MaxEncodedSizeToLookupInSet > 0 && set == null) 281 | throw new InvalidOperationException("StringSetOptions.MaxEncodedSizeToLookupInSet is greater than zero, but StringSet is null."); 282 | 283 | var encodedSize = (int)ReadVarUInt(); 284 | 285 | if (encodedSize == 0) 286 | { 287 | return nullable && ReadBoolean() ? null : ""; 288 | } 289 | 290 | var pos = _realPosition; 291 | var data = Data; 292 | 293 | if (pos + encodedSize > data.Length) 294 | throw new IndexOutOfRangeException(); 295 | 296 | _realPosition = pos + encodedSize; 297 | 298 | if (encoding == null) 299 | encoding = DefaultEncoding; 300 | 301 | if (encodedSize > stringSetOptions.MaxEncodedSizeToLookupInSet) 302 | { 303 | // don't care about interning, just read the string 304 | return encoding.GetString(Data, pos, encodedSize); 305 | } 306 | 307 | // we're going to use the StringSet as an intern pool 308 | 309 | var maxChars = encoding.GetMaxCharCount(encodedSize); 310 | var charBuffer = stackalloc char[maxChars]; 311 | 312 | int charsWritten; 313 | fixed (byte* dataPtr = data) 314 | { 315 | charsWritten = encoding.GetChars(&dataPtr[pos], encodedSize, charBuffer, maxChars); 316 | } 317 | 318 | // now that we've got the characters in a buffer, calculate the hash and see if it exists in the set 319 | if (stringSetOptions.PerformDangerousAutoAddToSet) 320 | { 321 | string str; 322 | set.Add(charBuffer, charsWritten, out str); 323 | return str; 324 | } 325 | else 326 | { 327 | var str = set.GetExistingString(charBuffer, charsWritten); 328 | return str ?? new string(charBuffer, 0, charsWritten); 329 | } 330 | } 331 | 332 | /// 333 | /// Writes a length-prefixed string to the stream. 334 | /// 335 | /// The string to write. 336 | /// True if the string is nullable. 337 | /// The encoding to use. If null, the ReusableStream's will be used. 338 | public void WriteString(string s, bool nullable, Encoding encoding = null) 339 | { 340 | if (s == null) 341 | { 342 | if (!nullable) 343 | throw new ArgumentNullException(nameof(s)); 344 | 345 | WriteNullString(); 346 | return; 347 | } 348 | 349 | if (s.Length == 0) 350 | { 351 | WriteZeroLengthString(nullable); 352 | return; 353 | } 354 | 355 | if (encoding == null) 356 | encoding = DefaultEncoding; 357 | 358 | // we actually have some characters to write 359 | var info = PrepareForStringWrite(s.Length, encoding); 360 | 361 | var data = Data; 362 | if (info.NeedsExactCount) 363 | { 364 | var realSize = encoding.GetByteCount(s); 365 | if (realSize > data.Length - info.StringPos) 366 | { 367 | data = Grow(info.StringPos + realSize); 368 | } 369 | } 370 | 371 | // if we've gotten here, then the data buffer is large enough to hold the string. Time to perform the write. 372 | var bytesWritten = encoding.GetBytes(s, 0, s.Length, data, info.StringPos); 373 | WriteVarIntImpl((ulong)bytesWritten, info.VarIntByteCount, info.VarIntPos); 374 | 375 | UpdateWritePosition(info.StringPos + bytesWritten); 376 | } 377 | 378 | /// 379 | /// Writes a length-prefixed string to the stream. 380 | /// 381 | /// The string to write. 382 | /// The index into where the string starts. 383 | /// The length of the string (in chars). 384 | /// True if the string is nullable. 385 | /// The encoding to use. If null, the ReusableStream's will be used. 386 | public void WriteString(char[] chars, int offset, int count, bool nullable, Encoding encoding = null) 387 | { 388 | if (chars == null) 389 | { 390 | if (!nullable) 391 | throw new ArgumentNullException(nameof(chars)); 392 | 393 | if (count != 0) 394 | throw new ArgumentOutOfRangeException(nameof(count), "Count must equal zero to write a null string."); 395 | 396 | WriteNullString(); 397 | return; 398 | } 399 | 400 | if (offset < 0 || offset > chars.Length) 401 | throw new ArgumentOutOfRangeException(nameof(offset)); 402 | 403 | if (count < 0 || offset + count > chars.Length) 404 | throw new ArgumentOutOfRangeException(nameof(count)); 405 | 406 | if (count == 0) 407 | { 408 | WriteZeroLengthString(nullable); 409 | return; 410 | } 411 | 412 | if (encoding == null) 413 | encoding = DefaultEncoding; 414 | 415 | // we actually have some characters to write 416 | var info = PrepareForStringWrite(count, encoding); 417 | 418 | var data = Data; 419 | if (info.NeedsExactCount) 420 | { 421 | var realSize = encoding.GetByteCount(chars, offset, count); 422 | if (realSize > data.Length - info.StringPos) 423 | { 424 | data = Grow(info.StringPos + realSize); 425 | } 426 | } 427 | 428 | // if we've gotten here, then the data buffer is large enough to hold the string. Time to perform the write. 429 | var bytesWritten = encoding.GetBytes(chars, offset, count, data, info.StringPos); 430 | WriteVarIntImpl((ulong)bytesWritten, info.VarIntByteCount, info.VarIntPos); 431 | 432 | UpdateWritePosition(info.StringPos + bytesWritten); 433 | } 434 | 435 | /// 436 | /// Writes a length-prefixed string to the stream. 437 | /// 438 | /// The string to write. 439 | /// The length of the string (in chars). 440 | /// True if the string is nullable. 441 | /// The encoding to use. If null, the ReusableStream's will be used. 442 | public unsafe void WriteString(char* chars, int count, bool nullable, Encoding encoding = null) 443 | { 444 | if (chars == null) 445 | { 446 | if (!nullable) 447 | throw new ArgumentNullException(nameof(chars)); 448 | 449 | if (count != 0) 450 | throw new ArgumentOutOfRangeException(nameof(count), "Count must equal zero to write a null string."); 451 | 452 | WriteNullString(); 453 | return; 454 | } 455 | 456 | if (count < 0) 457 | throw new ArgumentOutOfRangeException(nameof(count)); 458 | 459 | if (count == 0) 460 | { 461 | WriteZeroLengthString(nullable); 462 | return; 463 | } 464 | 465 | if (encoding == null) 466 | encoding = DefaultEncoding; 467 | 468 | // we actually have some characters to write 469 | var info = PrepareForStringWrite(count, encoding); 470 | 471 | var data = Data; 472 | if (info.NeedsExactCount) 473 | { 474 | var realSize = encoding.GetByteCount(chars, count); 475 | if (realSize > data.Length - info.StringPos) 476 | { 477 | data = Grow(info.StringPos + realSize); 478 | } 479 | } 480 | 481 | // if we've gotten here, then the data buffer is large enough to hold the string. Time to perform the write. 482 | int bytesWritten; 483 | fixed (byte* dataPtr = data) 484 | { 485 | bytesWritten = encoding.GetBytes(chars, count, &dataPtr[info.StringPos], data.Length - info.StringPos); 486 | } 487 | 488 | WriteVarIntImpl((ulong)bytesWritten, info.VarIntByteCount, info.VarIntPos); 489 | 490 | UpdateWritePosition(info.StringPos + bytesWritten); 491 | } 492 | 493 | void WriteNullString() 494 | { 495 | int pos, newPos; 496 | EnsureRoomFor(2, out pos, out newPos); 497 | 498 | var data = Data; 499 | data[pos] = 0; // first byte indicates zero length 500 | data[pos + 1] = 1; // second byte = 1 indicates null 501 | 502 | UpdateWritePosition(newPos); 503 | } 504 | 505 | void WriteZeroLengthString(bool nullable) 506 | { 507 | int pos, newPos; 508 | 509 | if (nullable) 510 | { 511 | EnsureRoomFor(2, out pos, out newPos); 512 | 513 | var data = Data; 514 | data[pos] = 0; 515 | data[pos + 1] = 0; 516 | } 517 | else 518 | { 519 | EnsureRoomFor(1, out pos, out newPos); 520 | 521 | var data = Data; 522 | data[pos] = 0; 523 | } 524 | 525 | UpdateWritePosition(newPos); 526 | } 527 | 528 | struct StringPrepInfo 529 | { 530 | public int VarIntByteCount; 531 | public int VarIntPos; 532 | public int StringPos; 533 | public bool NeedsExactCount; 534 | } 535 | 536 | StringPrepInfo PrepareForStringWrite(int count, Encoding encoding) 537 | { 538 | var info = default(StringPrepInfo); 539 | 540 | var maxSize = encoding.GetMaxByteCount(count); 541 | info.VarIntByteCount = BytesRequiredForVarInt((ulong)maxSize); 542 | info.VarIntPos = _realPosition; 543 | 544 | info.StringPos = info.VarIntPos + info.VarIntByteCount; // leave a spot to write the length prefix 545 | 546 | var data = Data; 547 | var remainingInBuffer = data.Length - info.StringPos; 548 | 549 | if (maxSize > remainingInBuffer) 550 | { 551 | // there might not be enough room in the data buffer 552 | var minSize = GetMinimumEncodedByteCount(encoding, count); 553 | if (minSize > remainingInBuffer) 554 | { 555 | // there definitely isn't room for the string, attempt to grow to cover the max size 556 | Grow(info.StringPos + maxSize); 557 | } 558 | else 559 | { 560 | // Either we don't know what the minimum size of this encoding is, or the minimum size is small enough to fit in 561 | // the data buffer. Either way, unfortunately we now have to check how big the string will actually be before we write it. 562 | info.NeedsExactCount = true; 563 | } 564 | } 565 | 566 | return info; 567 | } 568 | 569 | static int? GetMinimumEncodedByteCount(Encoding encoding, int charCount) 570 | { 571 | if (encoding is UTF8Encoding || encoding is ASCIIEncoding || encoding is UTF7Encoding) 572 | return charCount; 573 | 574 | // in theory, if every pair of characters were surrogate pairs, UTF32 could encode to the same size as UTF16 575 | if (encoding is UnicodeEncoding || encoding is UTF32Encoding) 576 | return charCount * 2; 577 | 578 | // don't know anything about this encoding 579 | return null; 580 | } 581 | } 582 | } 583 | -------------------------------------------------------------------------------- /PerformanceTypes/ReusableStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | 6 | namespace PerformanceTypes 7 | { 8 | /// 9 | /// Defines options for string-interning. 10 | /// 11 | public struct StringSetOptions 12 | { 13 | /// 14 | /// For any string whose encoded size (in bytes) is less than or equal to this value, a lookup in StringSet will be performed before allocating a new 15 | /// string. If the string already exists in StringSet, then no allocation occurs. If it does not exist in StringSet, or if its encoded size is larger 16 | /// than this value, then a new string is allocated. Newly allocated strings are NOT automatically added to StringSet unless 17 | /// PerformDangerousAutoAddToSet is true. 18 | /// 19 | /// For performance reasons, it is recommended to use a small value, such as 256, or less. Use zero to disable StringSet lookups altogether. 20 | /// 21 | public int MaxEncodedSizeToLookupInSet { get; set; } 22 | /// 23 | /// NEVER use this option when reading user-provided or arbitrary input, unless you plan to recycle the StringSet (allow it to be garbage collected). 24 | /// Failing to follow that advice could lead to unbounded memory growth. It is recommended that you avoid this option unless you understand the 25 | /// implications and risks. 26 | /// Does not allocate a new string if it already exists in the StringSet. Newly allocated strings are automatically added to the StringSet. 27 | /// Only applies to strings whose encoded size is less than MaxEncodedSizeToLookupInSet (in bytes). 28 | /// 29 | public bool PerformDangerousAutoAddToSet { get; set; } 30 | } 31 | 32 | /// 33 | /// A stream implementation where the underlying data (a byte array) can be swapped out, and the stream can be reset for re-reading or writing. 34 | /// 35 | public partial class ReusableStream : Stream 36 | { 37 | /// 38 | /// The current index into . It is equal to plus . 39 | /// 40 | int _realPosition; 41 | int _endPosition; // offset + length used 42 | 43 | StringSetOptions _defaultStringSetOptions; 44 | 45 | /// 46 | /// The underlying data for the stream. To replace this underlying data, call . 47 | /// 48 | public byte[] Data { get; private set; } 49 | /// 50 | /// True if the stream can automatically resize 51 | /// 52 | public bool CanGrow { get; private set; } 53 | 54 | /// 55 | /// True if the stream can be read from. 56 | /// 57 | public override bool CanRead => true; 58 | /// 59 | /// True if the stream can seek to a random byte location. 60 | /// 61 | public override bool CanSeek => true; 62 | /// 63 | /// True if the stream can be written to. 64 | /// 65 | public override bool CanWrite => true; 66 | 67 | /// 68 | /// The total size that the stream could grow to without needing to resize its underlying data array. 69 | /// 70 | public long Capacity => Data.LongLength - Offset; 71 | /// 72 | /// The total length of the stream (bytes written). 73 | /// 74 | public override long Length => _endPosition - Offset; 75 | /// 76 | /// The offset into data which represents Position = 0 for the stream. 77 | /// 78 | public int Offset { get; private set; } 79 | /// 80 | /// The number of bytes which can be read before reaching the end of the stream. 81 | /// 82 | public int UnreadByteCount => _endPosition - _realPosition; 83 | 84 | /// 85 | /// Gets or sets the encoding used for reading and writing strings. 86 | /// 87 | public Encoding DefaultEncoding { get; set; } = Encoding.UTF8; 88 | /// 89 | /// A StringSet which can be used as a string intern pool. Usage of this is configured via the StringSetOptions struct which is passed to 90 | /// SetDefaultStringSetOptions(), or as an optional parameter to the ReadString() methods. 91 | /// 92 | public StringSet StringSet { get; set; } 93 | 94 | /// 95 | /// The current byte position of the stream. This value is not necessarily the same as the current index of the underlying data. 96 | /// 97 | public override long Position 98 | { 99 | get { return _realPosition - Offset; } 100 | set 101 | { 102 | if (value > Capacity) 103 | throw new Exception("Position cannot be greater than capacity"); 104 | 105 | _realPosition = Offset + (int)value; 106 | } 107 | } 108 | 109 | /// 110 | /// WARNING: This constructor creates a ReusableStream, but does NOT initialize an underlying data array. You MUST call ReplaceData() prior to using the stream. 111 | /// 112 | public ReusableStream() 113 | { 114 | } 115 | 116 | 117 | /// 118 | /// Creates a ReusableStream and initializes and underlying data array. 119 | /// 120 | /// Initial size (in bytes) of the underlying data array. 121 | /// True to allow the underlying data array to be automatically replaced when additional capacity is needed. 122 | public ReusableStream(int capacity, bool canGrow = true) 123 | { 124 | if (capacity < 0) 125 | throw new ArgumentOutOfRangeException(nameof(capacity)); 126 | 127 | ReplaceData(new byte[capacity], 0, 0, canGrow); 128 | } 129 | 130 | /// 131 | /// Creates a ReusableStream using the provided data array. 132 | /// 133 | /// The array where data will be stored. 134 | /// The offset into data which represents Position = 0 for the stream. 135 | /// 136 | /// The number of bytes after offset which represents meaningful data for the stream. Note: this value only affects reading. If the stream is intended 137 | /// for writing, the length should typically be zero. 138 | /// 139 | /// True to allow the underlying data array to be automatically replaced when additional capacity is needed. 140 | public ReusableStream(byte[] data, int offset, int length, bool canGrow = false) 141 | { 142 | ReplaceData(data, offset, length, canGrow); 143 | } 144 | 145 | /// 146 | /// Replaces the underlying data array and resets Position to zero. 147 | /// 148 | /// The replacement data array. 149 | /// The offset into data which represents Position = 0 for the stream. 150 | /// 151 | /// The number of bytes after offset which represents meaningful data for the stream. Note: this value only affects reading. If the stream is intended 152 | /// for writing, the length should typically be zero. 153 | /// 154 | /// True to allow the underlying data array to be automatically replaced when additional capacity is needed. 155 | public void ReplaceData(byte[] data, int offset, int length, bool canGrow = false) 156 | { 157 | if (data == null) 158 | throw new ArgumentNullException(nameof(data)); 159 | 160 | if (offset < 0 || offset > data.Length) 161 | throw new ArgumentOutOfRangeException(nameof(offset)); 162 | 163 | if (length < 0 || offset + length > data.Length) 164 | throw new ArgumentOutOfRangeException(nameof(length)); 165 | 166 | CanGrow = canGrow; 167 | Data = data; 168 | Offset = offset; 169 | _realPosition = offset; 170 | _endPosition = offset + length; 171 | } 172 | 173 | /// 174 | /// Resets the length and position of the stream, effectively clearing out any existing data, and making it reusable. 175 | /// 176 | public void ResetForWriting() 177 | { 178 | _realPosition = Offset; 179 | _endPosition = Offset; 180 | } 181 | 182 | /// 183 | /// Resets position to zero, but does not clear data. 184 | /// 185 | public void ResetForReading() 186 | { 187 | _realPosition = Offset; 188 | } 189 | 190 | // not exposing this as a property because it's a struct and would likely cause surprising behavior to some users 191 | /// 192 | /// Gets the current default . This controls interning for strings read from the stream. Any updates to these options 193 | /// must be applied by calling . 194 | /// 195 | public StringSetOptions GetDefaultStringSetOptions() 196 | { 197 | return _defaultStringSetOptions; 198 | } 199 | 200 | /// 201 | /// Sets the default , which controls interning for strings read from the stream. If MaxEncodedSizeToLookupInSet is 202 | /// greater than zero, then must be non-null. 203 | /// 204 | /// 205 | public void SetDefaultStringSetOptions(StringSetOptions options) 206 | { 207 | if (options.MaxEncodedSizeToLookupInSet > 0 && StringSet == null) 208 | throw new InvalidOperationException("Cannot set MaxEncodedSizeToLookupInSet greater than zero while StringSet is null."); 209 | 210 | _defaultStringSetOptions = options; 211 | } 212 | 213 | /// 214 | /// This method is a no-op because there is nothing to flush to. 215 | /// 216 | public override void Flush() 217 | { 218 | } 219 | 220 | /// 221 | /// Reads, at most, bytes from the stream and copies them into at index . 222 | /// 223 | /// The destination to copy bytes to. 224 | /// The index in the destination buffer where the stream bytes should be copied to. 225 | /// The maximum number of bytes to read from the stream. Fewer bytes will be read and copied if the stream has fewer unread bytes remaining. 226 | /// The number of bytes actually read from the stream and copied. 227 | public override int Read(byte[] buffer, int offset, int count) 228 | { 229 | var readCount = Math.Min(count, UnreadByteCount); 230 | 231 | if (readCount > 0) 232 | { 233 | var pos = _realPosition; 234 | Array.Copy(Data, pos, buffer, offset, readCount); 235 | _realPosition = pos +readCount; 236 | } 237 | 238 | return readCount; 239 | } 240 | 241 | /// 242 | /// Reads, at most, from the stream and copies them to the unmanaged buffer. 243 | /// 244 | /// A pointer to an unmanaged buffer. 245 | /// The maximum number of bytes to copy from the stream to the buffer. 246 | /// The number of bytes copied (will be the minimum of either count or UreadByteCount). 247 | public unsafe int Read(byte* buffer, int count) 248 | { 249 | var readCount = Math.Min(count, UnreadByteCount); 250 | 251 | if (readCount > 0) 252 | { 253 | var pos = _realPosition; 254 | 255 | // In my testing, 400 bytes is around where it becomes worth it to call Marshal.Copy instead of manually copying. 256 | if (readCount > 400) 257 | { 258 | Marshal.Copy(Data, pos, (IntPtr)buffer, readCount); 259 | } 260 | else 261 | { 262 | fixed (byte* dataPtr = Data) 263 | { 264 | Unsafe.MemoryCopy(&dataPtr[pos], buffer, readCount); 265 | } 266 | } 267 | 268 | _realPosition = pos + readCount; 269 | } 270 | 271 | return readCount; 272 | } 273 | 274 | /// 275 | /// Reads bytes from starting at index and writes them to the stream. 276 | /// 277 | /// The buffer to read from. 278 | /// The index into buffer where to start reading from. 279 | /// The number of bytes to write to the stream. 280 | public override void Write(byte[] buffer, int offset, int count) 281 | { 282 | int pos, newPos; 283 | EnsureRoomFor(count, out pos, out newPos); 284 | 285 | Array.Copy(buffer, offset, Data, pos, count); 286 | 287 | UpdateWritePosition(newPos); 288 | } 289 | 290 | /// 291 | /// Reads bytes from and writes them to the stream. 292 | /// 293 | /// The buffer to read from. 294 | /// The number of bytes to write to the stream. 295 | public unsafe void Write(byte* buffer, int count) 296 | { 297 | int pos, newPos; 298 | EnsureRoomFor(count, out pos, out newPos); 299 | 300 | // In my testing, 400 bytes is around where it becomes worth it to call Marshal.Copy instead of manually copying. 301 | if (count > 400) 302 | { 303 | Marshal.Copy((IntPtr)buffer, Data, pos, count); 304 | } 305 | else 306 | { 307 | fixed (byte* dataPtr = Data) 308 | { 309 | Unsafe.MemoryCopy(buffer, &dataPtr[pos], count); 310 | } 311 | } 312 | 313 | UpdateWritePosition(newPos); 314 | } 315 | 316 | /// 317 | /// Writes a single byte to the stream. 318 | /// 319 | public override void WriteByte(byte value) 320 | { 321 | int pos, newPos; 322 | EnsureRoomFor(1, out pos, out newPos); 323 | 324 | Data[pos] = value; 325 | 326 | UpdateWritePosition(newPos); 327 | } 328 | 329 | /// 330 | /// Reads one byte from the stream and returns it. If there are no bytes remaining to read, -1 is returned. This behavior is as per the .NET Stream 331 | /// documentation. If this is not the behavior you're looking for, consider using instead. 332 | /// 333 | public override int ReadByte() 334 | { 335 | if (UnreadByteCount < 1) 336 | return -1; // as per the Stream documentation 337 | 338 | var pos = _realPosition; 339 | var value = Data[pos]; 340 | _realPosition = pos + 1; 341 | 342 | return value; 343 | } 344 | 345 | byte ReadOneByte() 346 | { 347 | if (UnreadByteCount < 1) 348 | throw new IndexOutOfRangeException(); 349 | 350 | var pos = _realPosition; 351 | var value = Data[pos]; 352 | _realPosition = pos + 1; 353 | 354 | return value; 355 | } 356 | 357 | /// 358 | /// Writes two bytes from to the stream. 359 | /// 360 | public unsafe void WriteTwoBytes(byte* src) 361 | { 362 | int pos, newPos; 363 | EnsureRoomFor(2, out pos, out newPos); 364 | 365 | var data = Data; 366 | data[pos + 0] = src[0]; 367 | data[pos + 1] = src[1]; 368 | 369 | UpdateWritePosition(newPos); 370 | } 371 | 372 | /// 373 | /// Writes four bytes from to the stream. 374 | /// 375 | public unsafe void WriteFourBytes(byte* src) 376 | { 377 | int pos, newPos; 378 | EnsureRoomFor(4, out pos, out newPos); 379 | 380 | var data = Data; 381 | data[pos + 0] = src[0]; 382 | data[pos + 1] = src[1]; 383 | data[pos + 2] = src[2]; 384 | data[pos + 3] = src[3]; 385 | 386 | UpdateWritePosition(newPos); 387 | } 388 | 389 | /// 390 | /// Writes eight bytes from to the stream. 391 | /// 392 | public unsafe void WriteEightBytes(byte* src) 393 | { 394 | int pos, newPos; 395 | EnsureRoomFor(8, out pos, out newPos); 396 | 397 | var data = Data; 398 | data[pos + 0] = src[0]; 399 | data[pos + 1] = src[1]; 400 | data[pos + 2] = src[2]; 401 | data[pos + 3] = src[3]; 402 | data[pos + 4] = src[4]; 403 | data[pos + 5] = src[5]; 404 | data[pos + 6] = src[6]; 405 | data[pos + 7] = src[7]; 406 | 407 | UpdateWritePosition(newPos); 408 | } 409 | 410 | /// 411 | /// Reads two bytes from the stream and copies them to . 412 | /// 413 | public unsafe void ReadTwoBytes(byte* dest) 414 | { 415 | if (UnreadByteCount < 2) 416 | throw new IndexOutOfRangeException(); 417 | 418 | var pos = _realPosition; 419 | var data = Data; 420 | 421 | dest[0] = data[pos + 0]; 422 | dest[1] = data[pos + 1]; 423 | 424 | _realPosition = pos + 2; 425 | } 426 | 427 | /// 428 | /// Reads four bytes from the stream and copies them to . 429 | /// 430 | public unsafe void ReadFourBytes(byte* dest) 431 | { 432 | if (UnreadByteCount < 4) 433 | throw new IndexOutOfRangeException(); 434 | 435 | var pos = _realPosition; 436 | var data = Data; 437 | 438 | dest[0] = data[pos + 0]; 439 | dest[1] = data[pos + 1]; 440 | dest[2] = data[pos + 2]; 441 | dest[3] = data[pos + 3]; 442 | 443 | _realPosition = pos + 4; 444 | } 445 | 446 | /// 447 | /// Reads eight bytes from the stream and copies them to . 448 | /// 449 | public unsafe void ReadEightBytes(byte* dest) 450 | { 451 | if (UnreadByteCount < 8) 452 | throw new IndexOutOfRangeException(); 453 | 454 | var pos = _realPosition; 455 | var data = Data; 456 | 457 | dest[0] = data[pos + 0]; 458 | dest[1] = data[pos + 1]; 459 | dest[2] = data[pos + 2]; 460 | dest[3] = data[pos + 3]; 461 | dest[4] = data[pos + 4]; 462 | dest[5] = data[pos + 5]; 463 | dest[6] = data[pos + 6]; 464 | dest[7] = data[pos + 7]; 465 | 466 | _realPosition = pos + 8; 467 | } 468 | 469 | /// 470 | /// Writes an unsigned integer 7-bits at a time. Requires between one and ten (inclusive) bytes depending on how large the integer is. 471 | /// 472 | public void WriteVarUInt(ulong value) 473 | { 474 | var byteCount = BytesRequiredForVarInt(value); 475 | 476 | int pos, newPos; 477 | EnsureRoomFor(byteCount, out pos, out newPos); 478 | WriteVarIntImpl(value, byteCount, pos); 479 | 480 | UpdateWritePosition(newPos); 481 | } 482 | 483 | /// 484 | /// Reads unsigned integers which were written using WriteVarUInt(). 485 | /// 486 | public ulong ReadVarUInt() 487 | { 488 | var data = Data; 489 | var pos = _realPosition; 490 | 491 | var lastByte = data[pos]; 492 | var value = (ulong)(lastByte & 0x7F); 493 | pos++; 494 | 495 | var shift = 0; 496 | while ((lastByte & 0x80) != 0) 497 | { 498 | lastByte = data[pos]; 499 | pos++; 500 | 501 | shift += 7; 502 | value |= (ulong)(lastByte & 0x7F) << shift; 503 | } 504 | 505 | // check if we read too far 506 | if (pos > _endPosition) 507 | throw new IndexOutOfRangeException(); 508 | 509 | _realPosition = pos; 510 | 511 | return value; 512 | } 513 | 514 | /// 515 | /// Sets the current index of the stream. This will not necessarily correspond with the index of the underlying data. 516 | /// 517 | public override long Seek(long offset, SeekOrigin origin) 518 | { 519 | switch (origin) 520 | { 521 | case SeekOrigin.Begin: 522 | Position = offset; 523 | break; 524 | case SeekOrigin.Current: 525 | Position += offset; 526 | break; 527 | case SeekOrigin.End: 528 | Position = Length + offset; 529 | break; 530 | } 531 | 532 | return Position; 533 | } 534 | 535 | /// 536 | /// Sets the length of the stream. Any data in the underlying data array is not altered. If there is not enough capacity in the underlying array and 537 | /// CanGrow is false, an exception will be thrown. 538 | /// 539 | public override void SetLength(long value) 540 | { 541 | if (value < 0 || value > int.MaxValue) 542 | throw new ArgumentOutOfRangeException(nameof(value)); 543 | 544 | var newEnd = Offset + (int)value; 545 | 546 | if (newEnd < 0) 547 | throw new ArgumentOutOfRangeException(nameof(value)); 548 | 549 | if (newEnd > Data.Length) 550 | Grow(newEnd); 551 | 552 | _endPosition = newEnd; 553 | 554 | if (_realPosition > newEnd) 555 | _realPosition = newEnd; 556 | } 557 | 558 | /// 559 | /// Checks if there is enough room to write . If not, it calls Grow(), which will throw an exception if CanGrow is false. 560 | /// 561 | void EnsureRoomFor(int additionalBytes, out int currentPos, out int newPos) 562 | { 563 | currentPos = _realPosition; 564 | newPos = currentPos + additionalBytes; 565 | 566 | if (newPos < 0) 567 | throw new OverflowException(); 568 | 569 | if (newPos > Data.Length) 570 | Grow(newPos); 571 | } 572 | 573 | byte[] Grow(int minimumSize) 574 | { 575 | if (!CanGrow) 576 | throw new IndexOutOfRangeException($"ReusableStream cannot grow to the required size ({minimumSize}) because CanGrow is false"); 577 | 578 | if (minimumSize < 0) 579 | throw new ArgumentOutOfRangeException(nameof(minimumSize)); 580 | 581 | var oldData = Data; 582 | var oldSize = oldData.Length; 583 | 584 | var newSize = oldSize; 585 | do 586 | { 587 | newSize *= 2; 588 | 589 | } while (newSize < minimumSize && newSize > oldSize); 590 | 591 | if (newSize < minimumSize) // could happen if there is an overflow 592 | newSize = minimumSize; 593 | 594 | var newData = new byte[newSize]; 595 | Array.Copy(oldData, newData, oldSize); 596 | Data = newData; 597 | 598 | return newData; 599 | } 600 | 601 | void UpdateWritePosition(int newPos) 602 | { 603 | _realPosition = newPos; 604 | 605 | if (newPos > _endPosition) 606 | _endPosition = newPos; 607 | } 608 | 609 | /// 610 | /// Writes the VarUInt at the real index specificed by . Does not modify _realPosition. 611 | /// 612 | /// The value of the integer being written. 613 | /// The number of bytes to write. This should be pre-calculated using BytesRequiredForVarInt(). 614 | /// The real position to begin writing the value. 615 | void WriteVarIntImpl(ulong value, int byteCount, int pos) 616 | { 617 | var data = Data; 618 | 619 | var endMinusOne = pos + byteCount - 1; 620 | while (pos < endMinusOne) 621 | { 622 | data[pos] = (byte)((value & 0x7F) | 0x80); 623 | value >>= 7; 624 | pos++; 625 | } 626 | 627 | // the last byte is just what's left in value 628 | data[pos] = (byte)value; 629 | } 630 | 631 | static int BytesRequiredForVarInt(ulong value) 632 | { 633 | // Binary search would be O(log n) instead of O(n) for linear search, but most values you would want to write with a VarInt 634 | // are integers which represent a count of something, and those are going to tend to be low-value integers, so linear should 635 | // be better for the common case. 636 | 637 | if ((value & ~0x7FUL) == 0) 638 | return 1; 639 | if ((value & ~0x3FFFUL) == 0) 640 | return 2; 641 | if ((value & ~0x1FFFFFUL) == 0) 642 | return 3; 643 | if ((value & ~0xFFFFFFFUL) == 0) 644 | return 4; 645 | if ((value & ~0x7FFFFFFFFUL) == 0) 646 | return 5; 647 | if ((value & ~0x3FFFFFFFFFFUL) == 0) 648 | return 6; 649 | if ((value & ~0x1FFFFFFFFFFFFUL) == 0) 650 | return 7; 651 | if ((value & ~0xFFFFFFFFFFFFFFUL) == 0) 652 | return 8; 653 | if ((value & ~0x7FFFFFFFFFFFFFFFUL) == 0) 654 | return 9; 655 | 656 | return 10; 657 | } 658 | } 659 | } -------------------------------------------------------------------------------- /PerformanceTypes/StopwatchStruct.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Runtime.Versioning; 4 | 5 | namespace PerformanceTypes 6 | { 7 | /// 8 | /// A value-type implementation of Stopwatch. It allows for high precision benchmarking without needing to instantiate reference types on the managed heap. 9 | /// Because of native calls to QueryPerformanceCounter, this struct will only work on Windows. 10 | /// 11 | /// Note: because this is a struct, instances will be passed by value. This means every time you assign an instance to another variable, or pass it as a 12 | /// parameter, a copy is made. Calling methods like Stop() on one copy, will not have any effect on other copies. Unless you are comfortable with the 13 | /// implications of a pass-by-value mutable struct, it is highly recommended that you only assign each instance to one variable, and do not pass it to 14 | /// other methods. 15 | /// 16 | public struct StopwatchStruct 17 | { 18 | long _startTimestamp; 19 | long _elapsedCounts; 20 | 21 | /// 22 | /// True if the stopwatch is running. In other words, Start() has been called, but Stop() has not. 23 | /// 24 | public bool IsRunning { get; private set; } 25 | 26 | /// 27 | /// The number of "counts" that the stopwatch has run. This property is accurate, even while the stopwatch is running. 28 | /// 29 | public long ElapsedCounts 30 | { 31 | get 32 | { 33 | if (IsRunning) 34 | { 35 | long timestamp; 36 | QueryPerformanceCounter(out timestamp); 37 | return _elapsedCounts + (timestamp - _startTimestamp); 38 | } 39 | 40 | return _elapsedCounts; 41 | } 42 | } 43 | 44 | /// 45 | /// Gets the total elapsed time that the stopwatch has run. This property is accurate, even while the stopwatch is running. 46 | /// 47 | public TimeSpan Elapsed => TimeSpan.FromMilliseconds(GetElapsedMilliseconds()); 48 | 49 | /// 50 | /// Starts the stopwatch by marking the start time. 51 | /// 52 | public void Start() 53 | { 54 | if (!CanUse) 55 | throw new Exception("Unable to use QueryPerformanceCounter. The StopwatchStruct only supports high-resolution timings currently."); 56 | 57 | if (!IsRunning) 58 | { 59 | IsRunning = true; 60 | QueryPerformanceCounter(out _startTimestamp); 61 | } 62 | } 63 | 64 | /// 65 | /// Stops the stopwatch, if it was running. Calculates the elapsed ticks since it was last started, and adds them to . 66 | /// 67 | public void Stop() 68 | { 69 | if (IsRunning) 70 | { 71 | long timestamp; 72 | QueryPerformanceCounter(out timestamp); 73 | IsRunning = false; 74 | _elapsedCounts += timestamp - _startTimestamp; 75 | } 76 | } 77 | 78 | /// 79 | /// Returns the total number of milliseconds that the stopwatch has run for. This property is accurate, even while the stopwatch is running. 80 | /// 81 | public double GetElapsedMilliseconds() 82 | { 83 | return ElapsedCounts / CountsPerMillisecond; 84 | } 85 | 86 | /**************************************************************************************** 87 | * 88 | * Static Members 89 | * 90 | ****************************************************************************************/ 91 | 92 | /// 93 | /// Returns true if the Stopwatch struct can be used on the current system. Only Windows is supported. 94 | /// 95 | public static bool CanUse { get; } 96 | /// 97 | /// True if high-resolution timing is available. Currently this is always true if is true. 98 | /// 99 | public static bool IsHighResolution => CanUse; 100 | /// 101 | /// The resolution of QueryPerformanceCounter (in "counts" per second). 102 | /// 103 | public static long CountsPerSecond { get; } 104 | /// 105 | /// The resolution of QueryPerformanceCounter (in "counts" per second). 106 | /// 107 | public static double CountsPerMillisecond { get; } 108 | /// 109 | /// If an exception occurred during initialization, it will be available here. 110 | /// 111 | public static Exception InitializationException { get; } 112 | 113 | static StopwatchStruct() 114 | { 115 | try 116 | { 117 | long countsPerSecond; 118 | var succeeded = QueryPerformanceFrequency(out countsPerSecond); 119 | 120 | if (succeeded) 121 | { 122 | CanUse = true; 123 | CountsPerSecond = countsPerSecond; 124 | CountsPerMillisecond = countsPerSecond / 1000.0; 125 | } 126 | } 127 | catch (Exception ex) 128 | { 129 | InitializationException = ex; 130 | } 131 | } 132 | 133 | /// 134 | /// Calls the WinAPI QueryPerformanceCounter method (the Windows high-resolution time). 135 | /// See: https://msdn.microsoft.com/en-us/library/windows/desktop/ms644904(v=vs.85).aspx 136 | /// 137 | /// The current high-resolution time value in ticks. 138 | /// True if the call succeeded. 139 | [DllImport("kernel32.dll")] 140 | public static extern bool QueryPerformanceCounter(out long value); 141 | 142 | /// 143 | /// Calls the WinAPI QueryPerformanceFrequency method. This method allows you to determine the resolution of QueryPerformanceCounter. 144 | /// See: https://msdn.microsoft.com/en-us/library/windows/desktop/ms644905(v=vs.85).aspx 145 | /// 146 | /// The number of ticks 147 | /// True if the call succeeded. 148 | [DllImport("kernel32.dll")] 149 | public static extern bool QueryPerformanceFrequency(out long value); 150 | } 151 | } -------------------------------------------------------------------------------- /PerformanceTypes/StringHash.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PerformanceTypes 4 | { 5 | /// 6 | /// Allows a string to be hashed character by character using the FNV-1a hashing algorithm. 7 | /// Always use StringHash.Begin() or StringHash.GetHash() to instantiate. 8 | /// 9 | public struct StringHash : IEquatable 10 | { 11 | const uint FNV_PRIME = 16777619; 12 | const uint FNV_OFFSET_BASIS = 2166136261; 13 | 14 | /// 15 | /// The resulting value of the hash. 16 | /// 17 | public uint Value { get; private set; } 18 | 19 | /// 20 | /// Returns an initialized StringHash struct which can be used for iterating over characters via Iterate(). 21 | /// 22 | public static StringHash Begin() 23 | { 24 | var hash = new StringHash(); 25 | hash.Value = FNV_OFFSET_BASIS; 26 | return hash; 27 | } 28 | 29 | /// 30 | /// Allows you to iteratively calculate the hash value, one character at a time. 31 | /// 32 | public unsafe void Iterate(char c) 33 | { 34 | var bytes = (byte*)&c; 35 | var v = Value; 36 | 37 | v = unchecked((bytes[0] ^ v) * FNV_PRIME); 38 | v = unchecked((bytes[1] ^ v) * FNV_PRIME); 39 | 40 | Value = v; 41 | } 42 | 43 | /// 44 | /// Returns a calculated StringHash for the string. 45 | /// 46 | public static StringHash GetHash(string s) 47 | { 48 | if (s == null) 49 | throw new ArgumentNullException(nameof(s)); 50 | 51 | var hash = Begin(); 52 | foreach(var c in s) 53 | hash.Iterate(c); 54 | 55 | return hash; 56 | } 57 | 58 | /// 59 | /// Returns a calculated StringHash for the characters in the buffer. 60 | /// 61 | /// The characters which represent a string to hash. 62 | /// The offset into the buffer where your string starts. 63 | /// The length of the string you are hashing. 64 | public static StringHash GetHash(char[] buffer, int start, int count) 65 | { 66 | AssertBufferArgumentsAreSane(buffer.Length, start, count); 67 | 68 | var end = start + count; 69 | var hash = Begin(); 70 | for (var i = start; i < end; i++) 71 | { 72 | hash.Iterate(buffer[i]); 73 | } 74 | 75 | return hash; 76 | } 77 | 78 | /// 79 | /// Returns a calculated StringHash for chars pointed to by . 80 | /// 81 | /// A pointer to a character array. 82 | /// The number of characters to hash. 83 | /// 84 | public static unsafe StringHash GetHash(char* chars, int count) 85 | { 86 | var end = chars + count; 87 | var hash = Begin(); 88 | while (chars < end) 89 | { 90 | hash.Iterate(*chars); 91 | chars++; 92 | } 93 | 94 | return hash; 95 | } 96 | 97 | internal static void AssertBufferArgumentsAreSane(int bufferLength, int start, int count) 98 | { 99 | if (start < 0) 100 | throw new ArgumentOutOfRangeException(nameof(start), "Start argument cannot be negative"); 101 | 102 | if (count < 0) 103 | throw new ArgumentOutOfRangeException(nameof(count), "Length argument cannot be negative"); 104 | 105 | if (start + count > bufferLength) 106 | throw new Exception($"Start ({start}) plus length ({count}) arguments must be less than or equal to buffer.Length ({bufferLength})."); 107 | } 108 | 109 | /// 110 | /// Returns true if both StringHash operands have the same value. 111 | /// 112 | public static bool operator ==(StringHash a, StringHash b) 113 | { 114 | return a.Value == b.Value; 115 | } 116 | 117 | 118 | /// 119 | /// Returns true if both StringHash operands do not have the same value. 120 | /// 121 | public static bool operator !=(StringHash a, StringHash b) 122 | { 123 | return a.Value != b.Value; 124 | } 125 | 126 | /// 127 | /// Returns true if has the same value as the current StringHash. 128 | /// 129 | public bool Equals(StringHash other) 130 | { 131 | return this == other; 132 | } 133 | 134 | /// 135 | /// Returns true if is a Stringhash and has the same value as the current StringHash. 136 | /// 137 | public override bool Equals(object obj) 138 | { 139 | return obj is StringHash && Equals((StringHash)obj); 140 | } 141 | 142 | /// 143 | /// Returns the signed 32-bit integer value of the hash. 144 | /// 145 | public override int GetHashCode() 146 | { 147 | return (int)Value; 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /PerformanceTypes/StringSet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | 6 | namespace PerformanceTypes 7 | { 8 | /// 9 | /// A specialized hash set for storing strings. The primary purpose is to use as an intern pool. Strings can be looked up in the set by hash code, which 10 | /// means you can check if a string exists without having to allocate a new string. 11 | /// 12 | public class StringSet : IReadOnlyCollection 13 | { 14 | struct Slot 15 | { 16 | internal string Value; 17 | internal StringHash HashCode; 18 | internal int Next; 19 | } 20 | 21 | /// 22 | /// A cursor to a linked-list of strings. It provides a way to iterate over strings in which share the same hash value. 23 | /// 24 | public struct StringSearchCursor 25 | { 26 | internal object Slots; 27 | internal StringHash Hash; 28 | internal int SlotIndex; 29 | 30 | /// 31 | /// Indicates that there are more strings to search. However, the next call to NextString() may still return null even if MightHaveMore is true. 32 | /// NextString() will always return null if MightHaveMore is false. 33 | /// 34 | public bool MightHaveMore => SlotIndex >= 0; 35 | 36 | /// 37 | /// Returns the next string in the set with a matching Hash value, and advances the cursor. Returns null if there are no more matching strings. 38 | /// 39 | public string NextString() 40 | { 41 | var slots = (Slot[])Slots; 42 | 43 | while (MightHaveMore) 44 | { 45 | var index = SlotIndex; 46 | var value = slots[index].Value; 47 | var found = slots[index].HashCode == Hash; 48 | 49 | // update to the next index in the linked list 50 | SlotIndex = slots[index].Next; 51 | 52 | if (found) 53 | return value; 54 | } 55 | 56 | return null; 57 | } 58 | } 59 | 60 | class BucketsAndSlots 61 | { 62 | public readonly int[] Buckets; 63 | public readonly Slot[] Slots; 64 | public int NextAvailableSlotIndex; 65 | 66 | public BucketsAndSlots(int[] buckets, Slot[] slots, int nextAvailableSlotIndex) 67 | { 68 | Buckets = buckets; 69 | Slots = slots; 70 | NextAvailableSlotIndex = nextAvailableSlotIndex; 71 | } 72 | } 73 | 74 | // this extra indirection allows us to perform atomic grow operations (important to allow thread-safe reads without locking) 75 | BucketsAndSlots _data; 76 | 77 | readonly object _writeLock = new object(); 78 | 79 | /// 80 | /// Number of strings currently contained in the set. 81 | /// 82 | public int Count => _data.NextAvailableSlotIndex; 83 | /// 84 | /// The number of strings which can be held in the set before it will have to grow its internal data structures. 85 | /// 86 | public int MaxSize => _data.Slots.Length; 87 | 88 | /// 89 | /// Initializes an empty set. 90 | /// 91 | /// The initial number of strings the set can contain before having to resize its internal datastructures. 92 | public StringSet(int initialSize) 93 | { 94 | _data = new BucketsAndSlots(new int[initialSize], new Slot[initialSize], 0); 95 | } 96 | 97 | /// 98 | /// Returns an enumerator for all strings in the set. 99 | /// 100 | public IEnumerator GetEnumerator() 101 | { 102 | var slots = _data.Slots; 103 | foreach (var slot in slots) 104 | { 105 | var str = slot.Value; 106 | if (str != null) 107 | yield return str; 108 | } 109 | } 110 | 111 | IEnumerator IEnumerable.GetEnumerator() 112 | { 113 | return GetEnumerator(); 114 | } 115 | 116 | /// 117 | /// Returns a search cursor which allows you to iterate over every string in the set with the same StringHash. 118 | /// 119 | public StringSearchCursor GetSearchCursor(StringHash hash) 120 | { 121 | var data = _data; 122 | var cursor = new StringSearchCursor(); 123 | cursor.Slots = data.Slots; 124 | cursor.Hash = hash; 125 | 126 | var buckets = data.Buckets; 127 | var bucket = hash.Value % buckets.Length; 128 | cursor.SlotIndex = buckets[bucket] - 1; 129 | 130 | return cursor; 131 | } 132 | 133 | /// 134 | /// Adds a string to the set if it does not already exist. 135 | /// 136 | /// The string to add to the set. 137 | /// (optional) If the StringHash for str has already been calculated, you can provide it here to save re-calculation. 138 | /// True if the string was added. False if the string already existed in the set. 139 | public bool Add(string str, StringHash knownHashValue = default(StringHash)) 140 | { 141 | if (knownHashValue == default(StringHash)) 142 | knownHashValue = StringHash.GetHash(str); 143 | 144 | if (!ContainsString(str, knownHashValue)) 145 | { 146 | lock (_writeLock) 147 | { 148 | if (!ContainsString(str, knownHashValue)) 149 | { 150 | AddImpl(str, knownHashValue); 151 | return true; 152 | } 153 | } 154 | } 155 | 156 | return false; 157 | } 158 | 159 | /// 160 | /// Adds a string to the set if it does not already exist. 161 | /// 162 | /// The character array which represents the string you want to add. 163 | /// The index in the character array where your string starts. 164 | /// The length of the string you want to add. 165 | /// The string object representation of the characters. A new string is only allocated when it does not already exist in the set. 166 | /// (optional) If the StringHash has already been calculated, you can provide it here to save re-calculation. 167 | /// True if the string was added. False if the string already existed in the set. 168 | public bool Add(char[] buffer, int start, int count, out string str, StringHash knownHashValue = default(StringHash)) 169 | { 170 | if (knownHashValue == default(StringHash)) 171 | knownHashValue = StringHash.GetHash(buffer, start, count); 172 | else 173 | StringHash.AssertBufferArgumentsAreSane(buffer.Length, start, count); 174 | 175 | str = GetExistingStringImpl(buffer, start, count, knownHashValue); 176 | 177 | if (str != null) 178 | return false; // didn't add anything 179 | 180 | // an existing string wasn't found, we need to add it to the hash 181 | lock (_writeLock) 182 | { 183 | // first, check one more time to see if it exists 184 | str = GetExistingStringImpl(buffer, start, count, knownHashValue); 185 | 186 | if (str == null) 187 | { 188 | // it definitely doesn't exist. Let's add it 189 | str = new string(buffer, start, count); 190 | AddImpl(str, knownHashValue); 191 | return true; 192 | } 193 | 194 | return false; 195 | } 196 | } 197 | 198 | /// 199 | /// Adds a string to the set if it does not already exist. 200 | /// 201 | /// A pointer to the string you want to add. 202 | /// The length of the string (in chars). 203 | /// The string object representation of the characters. A new string is only allocated when it does not already exist in the set. 204 | /// (optional) If the StringHash has already been calculated, you can provide it here to save re-calculation. 205 | /// True if the string was added. False if the string already existed in the set. 206 | public unsafe bool Add(char* chars, int count, out string str, StringHash knownHashValue = default(StringHash)) 207 | { 208 | if (knownHashValue == default(StringHash)) 209 | knownHashValue = StringHash.GetHash(chars, count); 210 | 211 | str = GetExistingString(chars, count, knownHashValue); 212 | 213 | if (str != null) 214 | return false; // didn't add anything 215 | 216 | // an existing string wasn't found, we need to add it to the hash 217 | lock (_writeLock) 218 | { 219 | // first, check one more time to see if it exists 220 | str = GetExistingString(chars, count, knownHashValue); 221 | 222 | if (str == null) 223 | { 224 | // it definitely doesn't exist. Let's add it 225 | str = new string(chars, 0, count); 226 | AddImpl(str, knownHashValue); 227 | return true; 228 | } 229 | 230 | return false; 231 | } 232 | } 233 | 234 | /// 235 | /// Uses the characters from a buffer to check whether a string exists in the set, and retrieve it if so. 236 | /// 237 | /// The character array which represents the string you want to check for. 238 | /// The index in the character array where your string starts. 239 | /// The length of the string you want to check for. 240 | /// (optional) If the StringHash has already been calculated, you can provide it here to save re-calculation. 241 | /// If found in the set, the existing string is returned. If not found, null is returned. 242 | public string GetExistingString(char[] buffer, int start, int count, StringHash knownHashValue = default(StringHash)) 243 | { 244 | if (knownHashValue == default(StringHash)) 245 | knownHashValue = StringHash.GetHash(buffer, start, count); 246 | else 247 | StringHash.AssertBufferArgumentsAreSane(buffer.Length, start, count); 248 | 249 | return GetExistingStringImpl(buffer, start, count, knownHashValue); 250 | } 251 | 252 | /// 253 | /// Uses the characters from a buffer to check whether a string exists in the set, and retrieve it if so. 254 | /// 255 | /// A pointer to the string to search for. 256 | /// The length of the string (in chars). 257 | /// (optional) If the StringHash has already been calculated, you can provide it here to save re-calculation. 258 | /// If found in the set, the existing string is returned. If not found, null is returned. 259 | public unsafe string GetExistingString(char* chars, int count, StringHash knownHashValue = default(StringHash)) 260 | { 261 | if (knownHashValue == default(StringHash)) 262 | knownHashValue = StringHash.GetHash(chars, count); 263 | 264 | var cursor = GetSearchCursor(knownHashValue); 265 | while (cursor.MightHaveMore) 266 | { 267 | var value = cursor.NextString(); 268 | if (value != null && UnsafeStringComparer.AreEqual(value, chars, count)) 269 | return value; 270 | } 271 | 272 | return null; 273 | } 274 | 275 | string GetExistingStringImpl(char[] buffer, int start, int length, StringHash hash) 276 | { 277 | var cursor = GetSearchCursor(hash); 278 | while (cursor.MightHaveMore) 279 | { 280 | var value = cursor.NextString(); 281 | if (value != null && UnsafeStringComparer.AreEqual(value, buffer, start, length)) 282 | return value; 283 | } 284 | 285 | return null; 286 | } 287 | 288 | bool ContainsString(string str, StringHash hash) 289 | { 290 | var cursor = GetSearchCursor(hash); 291 | while (cursor.MightHaveMore) 292 | { 293 | if (str == cursor.NextString()) 294 | return true; 295 | } 296 | 297 | return false; 298 | } 299 | 300 | void AddImpl(string s, StringHash hash) 301 | { 302 | var data = _data; 303 | if (data.NextAvailableSlotIndex == data.Slots.Length) 304 | { 305 | Grow(); 306 | data = _data; 307 | } 308 | 309 | var slots = data.Slots; 310 | var buckets = data.Buckets; 311 | 312 | var bucket = hash.Value % slots.Length; 313 | var slotIndex = data.NextAvailableSlotIndex; 314 | data.NextAvailableSlotIndex++; 315 | 316 | slots[slotIndex].Value = s; 317 | slots[slotIndex].HashCode = hash; 318 | slots[slotIndex].Next = buckets[bucket] - 1; 319 | 320 | // The hash set would no longer be thread-safe on reads if somehow the bucket got reassigned before the slot was setup 321 | Thread.MemoryBarrier(); 322 | 323 | buckets[bucket] = slotIndex + 1; 324 | } 325 | 326 | void Grow() 327 | { 328 | var oldData = _data; 329 | var oldSize = oldData.Slots.Length; 330 | var newSize = oldSize * 2; 331 | 332 | var newSlots = new Slot[newSize]; 333 | Array.Copy(oldData.Slots, newSlots, oldSize); 334 | 335 | var newBuckets = new int[newSize]; 336 | for (var i = 0; i < oldSize; i++) 337 | { 338 | var bucket = newSlots[i].HashCode.Value % newSize; 339 | newSlots[i].Next = newBuckets[bucket] - 1; 340 | newBuckets[bucket] = i + 1; 341 | } 342 | 343 | _data = new BucketsAndSlots(newBuckets, newSlots, oldData.NextAvailableSlotIndex); 344 | } 345 | } 346 | } -------------------------------------------------------------------------------- /PerformanceTypes/Unsafe.cs: -------------------------------------------------------------------------------- 1 | namespace PerformanceTypes 2 | { 3 | /// 4 | /// Unsafe memory utilities. 5 | /// 6 | public static class Unsafe 7 | { 8 | /// 9 | /// This method is not a general-purpose replacement for memcpy or Marshal.Copy, however, it will generally out-perform those methods for byte lengths 10 | /// of around 400 or less. 11 | /// 12 | /// Buffer to copy from. 13 | /// Buffer to copy to. 14 | /// Number of bytes to copy. 15 | public static unsafe void MemoryCopy(byte* src, byte* dest, int count) 16 | { 17 | var remainder = count; 18 | 19 | if (count > 8) 20 | { 21 | var longs = count / 8; 22 | remainder = count % 8; 23 | 24 | var srcLong = (long*)src; 25 | var destLong = (long*)dest; 26 | 27 | while (longs > 0) 28 | { 29 | *destLong = *srcLong; 30 | srcLong++; 31 | destLong++; 32 | longs--; 33 | } 34 | 35 | src = (byte*)srcLong; 36 | dest = (byte*)destLong; 37 | } 38 | 39 | switch (remainder) 40 | { 41 | case 1: 42 | *dest = *src; 43 | return; 44 | case 2: 45 | *(short*)dest = *(short*)src; 46 | return; 47 | case 3: 48 | *(short*)dest = *(short*)src; 49 | *(dest + 2) = *(src + 2); 50 | return; 51 | case 4: 52 | *(int*)dest = *(int*)src; 53 | return; 54 | case 5: 55 | *(int*)dest = *(int*)src; 56 | dest[4] = src[4]; 57 | return; 58 | case 6: 59 | *(int*)dest = *(int*)src; 60 | *(short*)(dest + 4) = *(short*)(src + 4); 61 | return; 62 | case 7: 63 | *(int*)dest = *(int*)src; 64 | *(short*)(dest + 4) = *(short*)(src + 4); 65 | dest[6] = src[6]; 66 | return; 67 | case 8: 68 | *(long*)dest = *(long*)src; 69 | return; 70 | } 71 | } 72 | 73 | /// 74 | /// Returns a hexadecimal string representation of the buffer. 75 | /// 76 | /// The bytes to convert to a hex string. 77 | /// Length of the buffer (in bytes). 78 | public static unsafe string ToHexString(byte* buffer, int length) 79 | { 80 | const int numeric = '0'; 81 | const int alpha = 'a' - 10; 82 | 83 | var str = new string('0', length * 2); 84 | fixed (char* strPtr = str) 85 | { 86 | var c = strPtr; 87 | 88 | for (var i = 0; i < length; i++) 89 | { 90 | var high = buffer[i] >> 4; 91 | *c = (char)(high + (high < 10 ? numeric : alpha)); 92 | c++; 93 | 94 | var low = buffer[i] & 0xf; 95 | *c = (char)(low + (low < 10 ? numeric : alpha)); 96 | c++; 97 | } 98 | } 99 | 100 | return str; 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /PerformanceTypes/UnsafeMd5.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace PerformanceTypes 6 | { 7 | /// 8 | /// Represents the result of an MD5 hash operation. 9 | /// 10 | [StructLayout(LayoutKind.Explicit, Size = SIZE)] 11 | [SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Local")] 12 | public struct Md5Digest : IEquatable 13 | { 14 | [FieldOffset(0)] internal uint A; 15 | [FieldOffset(4)] internal uint B; 16 | [FieldOffset(8)] internal uint C; 17 | [FieldOffset(12)] internal uint D; 18 | 19 | [FieldOffset(0)] ulong _ab; 20 | [FieldOffset(8)] ulong _cd; 21 | 22 | /// 23 | /// The size of an struct (in bytes). 24 | /// 25 | public const int SIZE = 16; 26 | 27 | /// 28 | /// Returns the raw bytes from the struct. Note that is primarily intended to be used in unsafe context where you can simply 29 | /// take the address of the struct to get a byte pointer. This method will cause an allocation on the managed heap. 30 | /// 31 | public unsafe byte[] GetBytes() 32 | { 33 | var bytes = new byte[SIZE]; 34 | fixed (byte* ptr = bytes) 35 | { 36 | WriteBytes(ptr); 37 | } 38 | 39 | return bytes; 40 | } 41 | 42 | /// 43 | /// Writes the raw bytes from the digest into a byte buffer. The buffer must have at least 16 bytes available starting at . An 44 | /// exception is thrown when there are not enough bytes remaining. 45 | /// 46 | /// The buffer to write to. 47 | /// The index 48 | /// Always returns 16 (the number of bytes written). 49 | public unsafe int WriteBytes(byte[] buffer, int index = 0) 50 | { 51 | if (buffer == null) 52 | throw new ArgumentNullException(nameof(buffer)); 53 | 54 | if (index + SIZE > buffer.Length) 55 | throw new InvalidOperationException($"Buffer is not large enough to write 16 bytes at index {index}. Length = {buffer.Length}"); 56 | 57 | fixed (byte* ptr = buffer) 58 | { 59 | return WriteBytes(&ptr[index]); 60 | } 61 | } 62 | 63 | /// 64 | /// Writes the raw bytes from the digest into a byte buffer. The buffer must have at least 16 bytes available. 65 | /// 66 | /// The buffer to write to. 67 | /// Always returns 16 (the number of bytes written). 68 | public unsafe int WriteBytes(byte* buffer) 69 | { 70 | var longPtr = (ulong*)buffer; 71 | 72 | longPtr[0] = _ab; 73 | longPtr[1] = _cd; 74 | 75 | return SIZE; 76 | } 77 | 78 | /// 79 | /// Returns true if both operands have the same value. 80 | /// 81 | public static bool operator ==(Md5Digest a, Md5Digest b) 82 | { 83 | return a._ab == b._ab && a._cd == b._cd; 84 | } 85 | 86 | 87 | /// 88 | /// Returns true if both operands do not have the same value. 89 | /// 90 | public static bool operator !=(Md5Digest a, Md5Digest b) 91 | { 92 | return !(a._ab == b._ab && a._cd == b._cd); 93 | } 94 | 95 | /// 96 | /// Returns true if has the same value as the current . 97 | /// 98 | public bool Equals(Md5Digest other) 99 | { 100 | return this == other; 101 | } 102 | 103 | /// 104 | /// Returns true if is a and has the same value as the current . 105 | /// 106 | public override bool Equals(object obj) 107 | { 108 | return obj is Md5Digest && Equals((Md5Digest)obj); 109 | } 110 | 111 | /// 112 | /// Returns the first 32-bits of the hash as a signed integer. 113 | /// 114 | public override int GetHashCode() 115 | { 116 | return (int)A; 117 | } 118 | 119 | /// 120 | /// Returns a hex representation of the digest. 121 | /// 122 | public override unsafe string ToString() 123 | { 124 | fixed (Md5Digest* digestPtr = &this) 125 | { 126 | return Unsafe.ToHexString((byte*)digestPtr, SIZE); 127 | } 128 | } 129 | } 130 | 131 | /// 132 | /// A collection of methods for calculating the MD5 of byte arrays without performing any managed heap allocations. 133 | /// 134 | public static class UnsafeMd5 135 | { 136 | [StructLayout(LayoutKind.Sequential)] 137 | [SuppressMessage("ReSharper", "FieldCanBeMadeReadOnly.Local")] 138 | struct Block 139 | { 140 | #pragma warning disable 169 // unused field 141 | ulong _zero; 142 | ulong _one; 143 | ulong _two; 144 | ulong _three; 145 | ulong _four; 146 | ulong _five; 147 | ulong _six; 148 | ulong _seven; 149 | #pragma warning restore 169 150 | 151 | public void SetOriginalLength(int bytesCount) 152 | { 153 | _seven = (ulong)bytesCount * 8; 154 | } 155 | } 156 | 157 | /// 158 | /// WARNING: this method will only work correctly on little-endian architectures. 159 | /// Calculates the MD5 hash of the input. 160 | /// 161 | /// The byte array to hash. 162 | /// The result of the hash function. 163 | public static unsafe void ComputeHash(byte[] input, out Md5Digest digest) 164 | { 165 | fixed (Md5Digest* digestPtr = &digest) 166 | fixed (byte* ptr = input) 167 | { 168 | ComputeHash(ptr, input.Length, digestPtr); 169 | } 170 | } 171 | 172 | /// 173 | /// WARNING: this method will only work correctly on little-endian architectures. 174 | /// Calculates the MD5 hash of the input. 175 | /// 176 | /// The input (byte array) to hash. 177 | /// The length of the input in bytes. 178 | /// The result of the hash function. 179 | public static unsafe void ComputeHash(byte* input, int length, Md5Digest* digest) 180 | { 181 | const int bytesPerBlock = 64; 182 | var blocksCount = (length + 8) / bytesPerBlock + 1; 183 | 184 | digest->A = 0x67452301; 185 | digest->B = 0xefcdab89; 186 | digest->C = 0x98badcfe; 187 | digest->D = 0x10325476; 188 | 189 | var paddingBlockData = default(Block); 190 | 191 | for (var blockIndex = 0; blockIndex < blocksCount; blockIndex++) 192 | { 193 | var offset = blockIndex * bytesPerBlock; 194 | var blockEnd = offset + bytesPerBlock; 195 | 196 | uint* blockPtr; 197 | 198 | if (blockEnd > length) // we're going to run out of input on this block 199 | { 200 | if (offset >= length) // we're already totally past the input at this point - this block is just padding, and definitely the final block 201 | { 202 | if (offset == length) 203 | { 204 | // the end of input perfectly lined up with a block - so this is the start of padding 205 | *(byte*)&paddingBlockData = 0x80; 206 | } 207 | else 208 | { 209 | // this is not the start of padding, so we need to clear out any data from the first padding block 210 | paddingBlockData = default(Block); 211 | } 212 | 213 | paddingBlockData.SetOriginalLength(length); 214 | } 215 | else // there is still some input left to consume before we get to padding 216 | { 217 | var bytesRemaining = bytesPerBlock - (blockEnd - length); 218 | var paddingPtr = (byte*)&paddingBlockData; 219 | Unsafe.MemoryCopy(&input[offset], paddingPtr, bytesRemaining); 220 | 221 | // add bit 222 | paddingPtr[bytesRemaining] = 0x80; 223 | 224 | // check if we can add the length 225 | if (bytesPerBlock - (bytesRemaining + 1) >= 8) 226 | paddingBlockData.SetOriginalLength(length); 227 | 228 | } 229 | 230 | blockPtr = (uint*)&paddingBlockData; 231 | } 232 | else 233 | { 234 | blockPtr = (uint*)&input[offset]; 235 | } 236 | 237 | var a = digest->A; 238 | var b = digest->B; 239 | var c = digest->C; 240 | var d = digest->D; 241 | 242 | uint f; 243 | 244 | // 0 (a, b, c, d) 245 | f = (c ^ d) & b ^ d; 246 | a += f; 247 | a += 0xd76aa478; 248 | a += blockPtr[0]; 249 | a = b + ((a << 7) | (a >> (32 - 7))); 250 | 251 | // 1 (d, a, b, c) 252 | f = (b ^ c) & a ^ c; 253 | d += f; 254 | d += 0xe8c7b756; 255 | d += blockPtr[1]; 256 | d = a + ((d << 12) | (d >> (32 - 12))); 257 | 258 | // 2 (c, d, a, b) 259 | f = (a ^ b) & d ^ b; 260 | c += f; 261 | c += 0x242070db; 262 | c += blockPtr[2]; 263 | c = d + ((c << 17) | (c >> (32 - 17))); 264 | 265 | // 3 (b, c, d, a) 266 | f = (d ^ a) & c ^ a; 267 | b += f; 268 | b += 0xc1bdceee; 269 | b += blockPtr[3]; 270 | b = c + ((b << 22) | (b >> (32 - 22))); 271 | 272 | // 4 (a, b, c, d) 273 | f = (c ^ d) & b ^ d; 274 | a += f; 275 | a += 0xf57c0faf; 276 | a += blockPtr[4]; 277 | a = b + ((a << 7) | (a >> (32 - 7))); 278 | 279 | // 5 (d, a, b, c) 280 | f = (b ^ c) & a ^ c; 281 | d += f; 282 | d += 0x4787c62a; 283 | d += blockPtr[5]; 284 | d = a + ((d << 12) | (d >> (32 - 12))); 285 | 286 | // 6 (c, d, a, b) 287 | f = (a ^ b) & d ^ b; 288 | c += f; 289 | c += 0xa8304613; 290 | c += blockPtr[6]; 291 | c = d + ((c << 17) | (c >> (32 - 17))); 292 | 293 | // 7 (b, c, d, a) 294 | f = (d ^ a) & c ^ a; 295 | b += f; 296 | b += 0xfd469501; 297 | b += blockPtr[7]; 298 | b = c + ((b << 22) | (b >> (32 - 22))); 299 | 300 | // 8 (a, b, c, d) 301 | f = (c ^ d) & b ^ d; 302 | a += f; 303 | a += 0x698098d8; 304 | a += blockPtr[8]; 305 | a = b + ((a << 7) | (a >> (32 - 7))); 306 | 307 | // 9 (d, a, b, c) 308 | f = (b ^ c) & a ^ c; 309 | d += f; 310 | d += 0x8b44f7af; 311 | d += blockPtr[9]; 312 | d = a + ((d << 12) | (d >> (32 - 12))); 313 | 314 | // 10 (c, d, a, b) 315 | f = (a ^ b) & d ^ b; 316 | c += f; 317 | c += 0xffff5bb1; 318 | c += blockPtr[10]; 319 | c = d + ((c << 17) | (c >> (32 - 17))); 320 | 321 | // 11 (b, c, d, a) 322 | f = (d ^ a) & c ^ a; 323 | b += f; 324 | b += 0x895cd7be; 325 | b += blockPtr[11]; 326 | b = c + ((b << 22) | (b >> (32 - 22))); 327 | 328 | // 12 (a, b, c, d) 329 | f = (c ^ d) & b ^ d; 330 | a += f; 331 | a += 0x6b901122; 332 | a += blockPtr[12]; 333 | a = b + ((a << 7) | (a >> (32 - 7))); 334 | 335 | // 13 (d, a, b, c) 336 | f = (b ^ c) & a ^ c; 337 | d += f; 338 | d += 0xfd987193; 339 | d += blockPtr[13]; 340 | d = a + ((d << 12) | (d >> (32 - 12))); 341 | 342 | // 14 (c, d, a, b) 343 | f = (a ^ b) & d ^ b; 344 | c += f; 345 | c += 0xa679438e; 346 | c += blockPtr[14]; 347 | c = d + ((c << 17) | (c >> (32 - 17))); 348 | 349 | // 15 (b, c, d, a) 350 | f = (d ^ a) & c ^ a; 351 | b += f; 352 | b += 0x49b40821; 353 | b += blockPtr[15]; 354 | b = c + ((b << 22) | (b >> (32 - 22))); 355 | 356 | // 16 (a, b, c, d) 357 | f = (b ^ c) & d ^ c; 358 | a += f; 359 | a += 0xf61e2562; 360 | a += blockPtr[(5 * 16 + 1) & 0xf]; 361 | a = b + ((a << 5) | (a >> (32 - 5))); 362 | 363 | // 17 (d, a, b, c) 364 | f = (a ^ b) & c ^ b; 365 | d += f; 366 | d += 0xc040b340; 367 | d += blockPtr[(5 * 17 + 1) & 0xf]; 368 | d = a + ((d << 9) | (d >> (32 - 9))); 369 | 370 | // 18 (c, d, a, b) 371 | f = (d ^ a) & b ^ a; 372 | c += f; 373 | c += 0x265e5a51; 374 | c += blockPtr[(5 * 18 + 1) & 0xf]; 375 | c = d + ((c << 14) | (c >> (32 - 14))); 376 | 377 | // 19 (b, c, d, a) 378 | f = (c ^ d) & a ^ d; 379 | b += f; 380 | b += 0xe9b6c7aa; 381 | b += blockPtr[(5 * 19 + 1) & 0xf]; 382 | b = c + ((b << 20) | (b >> (32 - 20))); 383 | 384 | // 20 (a, b, c, d) 385 | f = (b ^ c) & d ^ c; 386 | a += f; 387 | a += 0xd62f105d; 388 | a += blockPtr[(5 * 20 + 1) & 0xf]; 389 | a = b + ((a << 5) | (a >> (32 - 5))); 390 | 391 | // 21 (d, a, b, c) 392 | f = (a ^ b) & c ^ b; 393 | d += f; 394 | d += 0x2441453; 395 | d += blockPtr[(5 * 21 + 1) & 0xf]; 396 | d = a + ((d << 9) | (d >> (32 - 9))); 397 | 398 | // 22 (c, d, a, b) 399 | f = (d ^ a) & b ^ a; 400 | c += f; 401 | c += 0xd8a1e681; 402 | c += blockPtr[(5 * 22 + 1) & 0xf]; 403 | c = d + ((c << 14) | (c >> (32 - 14))); 404 | 405 | // 23 (b, c, d, a) 406 | f = (c ^ d) & a ^ d; 407 | b += f; 408 | b += 0xe7d3fbc8; 409 | b += blockPtr[(5 * 23 + 1) & 0xf]; 410 | b = c + ((b << 20) | (b >> (32 - 20))); 411 | 412 | // 24 (a, b, c, d) 413 | f = (b ^ c) & d ^ c; 414 | a += f; 415 | a += 0x21e1cde6; 416 | a += blockPtr[(5 * 24 + 1) & 0xf]; 417 | a = b + ((a << 5) | (a >> (32 - 5))); 418 | 419 | // 25 (d, a, b, c) 420 | f = (a ^ b) & c ^ b; 421 | d += f; 422 | d += 0xc33707d6; 423 | d += blockPtr[(5 * 25 + 1) & 0xf]; 424 | d = a + ((d << 9) | (d >> (32 - 9))); 425 | 426 | // 26 (c, d, a, b) 427 | f = (d ^ a) & b ^ a; 428 | c += f; 429 | c += 0xf4d50d87; 430 | c += blockPtr[(5 * 26 + 1) & 0xf]; 431 | c = d + ((c << 14) | (c >> (32 - 14))); 432 | 433 | // 27 (b, c, d, a) 434 | f = (c ^ d) & a ^ d; 435 | b += f; 436 | b += 0x455a14ed; 437 | b += blockPtr[(5 * 27 + 1) & 0xf]; 438 | b = c + ((b << 20) | (b >> (32 - 20))); 439 | 440 | // 28 (a, b, c, d) 441 | f = (b ^ c) & d ^ c; 442 | a += f; 443 | a += 0xa9e3e905; 444 | a += blockPtr[(5 * 28 + 1) & 0xf]; 445 | a = b + ((a << 5) | (a >> (32 - 5))); 446 | 447 | // 29 (d, a, b, c) 448 | f = (a ^ b) & c ^ b; 449 | d += f; 450 | d += 0xfcefa3f8; 451 | d += blockPtr[(5 * 29 + 1) & 0xf]; 452 | d = a + ((d << 9) | (d >> (32 - 9))); 453 | 454 | // 30 (c, d, a, b) 455 | f = (d ^ a) & b ^ a; 456 | c += f; 457 | c += 0x676f02d9; 458 | c += blockPtr[(5 * 30 + 1) & 0xf]; 459 | c = d + ((c << 14) | (c >> (32 - 14))); 460 | 461 | // 31 (b, c, d, a) 462 | f = (c ^ d) & a ^ d; 463 | b += f; 464 | b += 0x8d2a4c8a; 465 | b += blockPtr[(5 * 31 + 1) & 0xf]; 466 | b = c + ((b << 20) | (b >> (32 - 20))); 467 | 468 | // 32 (a, b, c, d) 469 | f = b ^ c ^ d; 470 | a += f; 471 | a += 0xfffa3942; 472 | a += blockPtr[(3 * 32 + 5) & 0xf]; 473 | a = b + ((a << 4) | (a >> (32 - 4))); 474 | 475 | // 33 (d, a, b, c) 476 | f = a ^ b ^ c; 477 | d += f; 478 | d += 0x8771f681; 479 | d += blockPtr[(3 * 33 + 5) & 0xf]; 480 | d = a + ((d << 11) | (d >> (32 - 11))); 481 | 482 | // 34 (c, d, a, b) 483 | f = d ^ a ^ b; 484 | c += f; 485 | c += 0x6d9d6122; 486 | c += blockPtr[(3 * 34 + 5) & 0xf]; 487 | c = d + ((c << 16) | (c >> (32 - 16))); 488 | 489 | // 35 (b, c, d, a) 490 | f = c ^ d ^ a; 491 | b += f; 492 | b += 0xfde5380c; 493 | b += blockPtr[(3 * 35 + 5) & 0xf]; 494 | b = c + ((b << 23) | (b >> (32 - 23))); 495 | 496 | // 36 (a, b, c, d) 497 | f = b ^ c ^ d; 498 | a += f; 499 | a += 0xa4beea44; 500 | a += blockPtr[(3 * 36 + 5) & 0xf]; 501 | a = b + ((a << 4) | (a >> (32 - 4))); 502 | 503 | // 37 (d, a, b, c) 504 | f = a ^ b ^ c; 505 | d += f; 506 | d += 0x4bdecfa9; 507 | d += blockPtr[(3 * 37 + 5) & 0xf]; 508 | d = a + ((d << 11) | (d >> (32 - 11))); 509 | 510 | // 38 (c, d, a, b) 511 | f = d ^ a ^ b; 512 | c += f; 513 | c += 0xf6bb4b60; 514 | c += blockPtr[(3 * 38 + 5) & 0xf]; 515 | c = d + ((c << 16) | (c >> (32 - 16))); 516 | 517 | // 39 (b, c, d, a) 518 | f = c ^ d ^ a; 519 | b += f; 520 | b += 0xbebfbc70; 521 | b += blockPtr[(3 * 39 + 5) & 0xf]; 522 | b = c + ((b << 23) | (b >> (32 - 23))); 523 | 524 | // 40 (a, b, c, d) 525 | f = b ^ c ^ d; 526 | a += f; 527 | a += 0x289b7ec6; 528 | a += blockPtr[(3 * 40 + 5) & 0xf]; 529 | a = b + ((a << 4) | (a >> (32 - 4))); 530 | 531 | // 41 (d, a, b, c) 532 | f = a ^ b ^ c; 533 | d += f; 534 | d += 0xeaa127fa; 535 | d += blockPtr[(3 * 41 + 5) & 0xf]; 536 | d = a + ((d << 11) | (d >> (32 - 11))); 537 | 538 | // 42 (c, d, a, b) 539 | f = d ^ a ^ b; 540 | c += f; 541 | c += 0xd4ef3085; 542 | c += blockPtr[(3 * 42 + 5) & 0xf]; 543 | c = d + ((c << 16) | (c >> (32 - 16))); 544 | 545 | // 43 (b, c, d, a) 546 | f = c ^ d ^ a; 547 | b += f; 548 | b += 0x4881d05; 549 | b += blockPtr[(3 * 43 + 5) & 0xf]; 550 | b = c + ((b << 23) | (b >> (32 - 23))); 551 | 552 | // 44 (a, b, c, d) 553 | f = b ^ c ^ d; 554 | a += f; 555 | a += 0xd9d4d039; 556 | a += blockPtr[(3 * 44 + 5) & 0xf]; 557 | a = b + ((a << 4) | (a >> (32 - 4))); 558 | 559 | // 45 (d, a, b, c) 560 | f = a ^ b ^ c; 561 | d += f; 562 | d += 0xe6db99e5; 563 | d += blockPtr[(3 * 45 + 5) & 0xf]; 564 | d = a + ((d << 11) | (d >> (32 - 11))); 565 | 566 | // 46 (c, d, a, b) 567 | f = d ^ a ^ b; 568 | c += f; 569 | c += 0x1fa27cf8; 570 | c += blockPtr[(3 * 46 + 5) & 0xf]; 571 | c = d + ((c << 16) | (c >> (32 - 16))); 572 | 573 | // 47 (b, c, d, a) 574 | f = c ^ d ^ a; 575 | b += f; 576 | b += 0xc4ac5665; 577 | b += blockPtr[(3 * 47 + 5) & 0xf]; 578 | b = c + ((b << 23) | (b >> (32 - 23))); 579 | 580 | // 48 (a, b, c, d) 581 | f = c ^ (b | ~d); 582 | a += f; 583 | a += 0xf4292244; 584 | a += blockPtr[(7 * 48) & 0xf]; 585 | a = b + ((a << 6) | (a >> (32 - 6))); 586 | 587 | // 49 (d, a, b, c) 588 | f = b ^ (a | ~c); 589 | d += f; 590 | d += 0x432aff97; 591 | d += blockPtr[(7 * 49) & 0xf]; 592 | d = a + ((d << 10) | (d >> (32 - 10))); 593 | 594 | // 50 (c, d, a, b) 595 | f = a ^ (d | ~b); 596 | c += f; 597 | c += 0xab9423a7; 598 | c += blockPtr[(7 * 50) & 0xf]; 599 | c = d + ((c << 15) | (c >> (32 - 15))); 600 | 601 | // 51 (b, c, d, a) 602 | f = d ^ (c | ~a); 603 | b += f; 604 | b += 0xfc93a039; 605 | b += blockPtr[(7 * 51) & 0xf]; 606 | b = c + ((b << 21) | (b >> (32 - 21))); 607 | 608 | // 52 (a, b, c, d) 609 | f = c ^ (b | ~d); 610 | a += f; 611 | a += 0x655b59c3; 612 | a += blockPtr[(7 * 52) & 0xf]; 613 | a = b + ((a << 6) | (a >> (32 - 6))); 614 | 615 | // 53 (d, a, b, c) 616 | f = b ^ (a | ~c); 617 | d += f; 618 | d += 0x8f0ccc92; 619 | d += blockPtr[(7 * 53) & 0xf]; 620 | d = a + ((d << 10) | (d >> (32 - 10))); 621 | 622 | // 54 (c, d, a, b) 623 | f = a ^ (d | ~b); 624 | c += f; 625 | c += 0xffeff47d; 626 | c += blockPtr[(7 * 54) & 0xf]; 627 | c = d + ((c << 15) | (c >> (32 - 15))); 628 | 629 | // 55 (b, c, d, a) 630 | f = d ^ (c | ~a); 631 | b += f; 632 | b += 0x85845dd1; 633 | b += blockPtr[(7 * 55) & 0xf]; 634 | b = c + ((b << 21) | (b >> (32 - 21))); 635 | 636 | // 56 (a, b, c, d) 637 | f = c ^ (b | ~d); 638 | a += f; 639 | a += 0x6fa87e4f; 640 | a += blockPtr[(7 * 56) & 0xf]; 641 | a = b + ((a << 6) | (a >> (32 - 6))); 642 | 643 | // 57 (d, a, b, c) 644 | f = b ^ (a | ~c); 645 | d += f; 646 | d += 0xfe2ce6e0; 647 | d += blockPtr[(7 * 57) & 0xf]; 648 | d = a + ((d << 10) | (d >> (32 - 10))); 649 | 650 | // 58 (c, d, a, b) 651 | f = a ^ (d | ~b); 652 | c += f; 653 | c += 0xa3014314; 654 | c += blockPtr[(7 * 58) & 0xf]; 655 | c = d + ((c << 15) | (c >> (32 - 15))); 656 | 657 | // 59 (b, c, d, a) 658 | f = d ^ (c | ~a); 659 | b += f; 660 | b += 0x4e0811a1; 661 | b += blockPtr[(7 * 59) & 0xf]; 662 | b = c + ((b << 21) | (b >> (32 - 21))); 663 | 664 | // 60 (a, b, c, d) 665 | f = c ^ (b | ~d); 666 | a += f; 667 | a += 0xf7537e82; 668 | a += blockPtr[(7 * 60) & 0xf]; 669 | a = b + ((a << 6) | (a >> (32 - 6))); 670 | 671 | // 61 (d, a, b, c) 672 | f = b ^ (a | ~c); 673 | d += f; 674 | d += 0xbd3af235; 675 | d += blockPtr[(7 * 61) & 0xf]; 676 | d = a + ((d << 10) | (d >> (32 - 10))); 677 | 678 | // 62 (c, d, a, b) 679 | f = a ^ (d | ~b); 680 | c += f; 681 | c += 0x2ad7d2bb; 682 | c += blockPtr[(7 * 62) & 0xf]; 683 | c = d + ((c << 15) | (c >> (32 - 15))); 684 | 685 | // 63 (b, c, d, a) 686 | f = d ^ (c | ~a); 687 | b += f; 688 | b += 0xeb86d391; 689 | b += blockPtr[(7 * 63) & 0xf]; 690 | b = c + ((b << 21) | (b >> (32 - 21))); 691 | 692 | digest->A += a; 693 | digest->B += b; 694 | digest->C += c; 695 | digest->D += d; 696 | } 697 | } 698 | } 699 | } -------------------------------------------------------------------------------- /PerformanceTypes/UnsafeStringComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PerformanceTypes 4 | { 5 | /// 6 | /// Provides string comparison methods. The purpose is to be able to easily compare a string to a character array (or pointer) without needing to allocate 7 | /// a string. 8 | /// 9 | public static class UnsafeStringComparer 10 | { 11 | /// 12 | /// Compares a string to the characters in a buffer. 13 | /// 14 | /// The string to compare against the character buffer. 15 | /// The buffer to compare against the string. 16 | /// True if the characters in the string and buffer were equal. 17 | public static bool AreEqual(string str, char[] buffer) 18 | { 19 | return AreEqual(str, buffer, 0, buffer.Length); 20 | } 21 | 22 | /// 23 | /// Compares a string to the characters in a buffer. 24 | /// 25 | /// The string to compare against the character buffer. 26 | /// The buffer to compare against the string. 27 | /// The offset into the buffer where comparison with the string should start. 28 | /// 29 | /// The length of characters in the buffer to compare against the string. 30 | /// If length and str.Length don't match, this method will always return false 31 | /// 32 | /// True if the characters in the string and buffer were equal. 33 | public static unsafe bool AreEqual(string str, char[] buffer, int start, int count) 34 | { 35 | if (str.Length != count) 36 | return false; 37 | 38 | if (start + count > buffer.Length) 39 | return false; 40 | 41 | switch (count) 42 | { 43 | case 0: 44 | return true; 45 | case 1: 46 | return str[0] == buffer[start + 0]; 47 | case 2: 48 | return str[0] == buffer[start + 0] 49 | && str[1] == buffer[start + 1]; 50 | case 3: 51 | return str[0] == buffer[start + 0] 52 | && str[1] == buffer[start + 1] 53 | && str[2] == buffer[start + 2]; 54 | case 4: 55 | return str[0] == buffer[start + 0] 56 | && str[1] == buffer[start + 1] 57 | && str[2] == buffer[start + 2] 58 | && str[3] == buffer[start + 3]; 59 | case 5: 60 | return str[0] == buffer[start + 0] 61 | && str[1] == buffer[start + 1] 62 | && str[2] == buffer[start + 2] 63 | && str[3] == buffer[start + 3] 64 | && str[4] == buffer[start + 4]; 65 | case 6: 66 | return str[0] == buffer[start + 0] 67 | && str[1] == buffer[start + 1] 68 | && str[2] == buffer[start + 2] 69 | && str[3] == buffer[start + 3] 70 | && str[4] == buffer[start + 4] 71 | && str[5] == buffer[start + 5]; 72 | case 7: 73 | return str[0] == buffer[start + 0] 74 | && str[1] == buffer[start + 1] 75 | && str[2] == buffer[start + 2] 76 | && str[3] == buffer[start + 3] 77 | && str[4] == buffer[start + 4] 78 | && str[5] == buffer[start + 5] 79 | && str[6] == buffer[start + 6]; 80 | } 81 | 82 | // eight characters or more makes it worth it to switch into unsafe more and read by int64 instead of by char. 83 | fixed (char* sPtr = str) 84 | fixed (char* bPtr = buffer) 85 | { 86 | return CompareByLong(sPtr, bPtr + start, count); 87 | } 88 | } 89 | 90 | /// 91 | /// Compares a string to the characters in a buffer. 92 | /// 93 | /// The string to compare against the character buffer. 94 | /// 95 | /// A pointer to the first character to compare in a buffer. 96 | /// The buffer must have least length chars remaining to prevent reading out of bounds. 97 | /// 98 | /// The number of characters in the buffer to compare. If length does not equal str.Length, this method will always return false. 99 | /// True if the characters in the string and buffer were equal. 100 | public static unsafe bool AreEqual(string str, char* buffer, int count) 101 | { 102 | if (str.Length != count) 103 | return false; 104 | 105 | switch (count) 106 | { 107 | case 0: 108 | return true; 109 | case 1: 110 | return str[0] == buffer[0]; 111 | case 2: 112 | return str[0] == buffer[0] 113 | && str[1] == buffer[1]; 114 | case 3: 115 | return str[0] == buffer[0] 116 | && str[1] == buffer[1] 117 | && str[2] == buffer[2]; 118 | case 4: 119 | return str[0] == buffer[0] 120 | && str[1] == buffer[1] 121 | && str[2] == buffer[2] 122 | && str[3] == buffer[3]; 123 | case 5: 124 | return str[0] == buffer[0] 125 | && str[1] == buffer[1] 126 | && str[2] == buffer[2] 127 | && str[3] == buffer[3] 128 | && str[4] == buffer[4]; 129 | case 6: 130 | return str[0] == buffer[0] 131 | && str[1] == buffer[1] 132 | && str[2] == buffer[2] 133 | && str[3] == buffer[3] 134 | && str[4] == buffer[4] 135 | && str[5] == buffer[5]; 136 | case 7: 137 | return str[0] == buffer[0] 138 | && str[1] == buffer[1] 139 | && str[2] == buffer[2] 140 | && str[3] == buffer[3] 141 | && str[4] == buffer[4] 142 | && str[5] == buffer[5] 143 | && str[6] == buffer[6]; 144 | default: 145 | fixed (char* sPtr = str) 146 | { 147 | return CompareByLong(sPtr, buffer, count); 148 | } 149 | } 150 | } 151 | 152 | /// 153 | /// Compares two character buffers against each other. 154 | /// 155 | /// A pointer to the first character to compare in the first buffer. 156 | /// A pointer to the first character to compare in the second buffer. 157 | /// The number of characters to compare in each buffer. 158 | /// True if the characters in the two buffers were equal. 159 | public static unsafe bool AreEqual(char* aPtr, char* bPtr, int count) 160 | { 161 | if (aPtr == bPtr) // they point to the same memory, so they're definitely equal 162 | return true; 163 | 164 | switch (count) 165 | { 166 | case 0: 167 | return true; 168 | case 1: 169 | return aPtr[0] == bPtr[0]; 170 | case 2: 171 | return aPtr[0] == bPtr[0] 172 | && aPtr[1] == bPtr[1]; 173 | case 3: 174 | return aPtr[0] == bPtr[0] 175 | && aPtr[1] == bPtr[1] 176 | && aPtr[2] == bPtr[2]; 177 | case 4: 178 | return aPtr[0] == bPtr[0] 179 | && aPtr[1] == bPtr[1] 180 | && aPtr[2] == bPtr[2] 181 | && aPtr[3] == bPtr[3]; 182 | case 5: 183 | return aPtr[0] == bPtr[0] 184 | && aPtr[1] == bPtr[1] 185 | && aPtr[2] == bPtr[2] 186 | && aPtr[3] == bPtr[3] 187 | && aPtr[4] == bPtr[4]; 188 | case 6: 189 | return aPtr[0] == bPtr[0] 190 | && aPtr[1] == bPtr[1] 191 | && aPtr[2] == bPtr[2] 192 | && aPtr[3] == bPtr[3] 193 | && aPtr[4] == bPtr[4] 194 | && aPtr[5] == bPtr[5]; 195 | case 7: 196 | return aPtr[0] == bPtr[0] 197 | && aPtr[1] == bPtr[1] 198 | && aPtr[2] == bPtr[2] 199 | && aPtr[3] == bPtr[3] 200 | && aPtr[4] == bPtr[4] 201 | && aPtr[5] == bPtr[5] 202 | && aPtr[6] == bPtr[6]; 203 | default: 204 | return CompareByLong(aPtr, bPtr, count); 205 | } 206 | } 207 | 208 | 209 | static unsafe bool CompareByLong(char* aPtr, char* bPtr, int count) 210 | { 211 | const int divisor = sizeof(long) / sizeof(char); 212 | var longs = count / divisor; 213 | var remainder = count % divisor; 214 | 215 | var aLong = (long*)aPtr; 216 | var bLong = (long*)bPtr; 217 | 218 | var longsEnd = aLong + longs; 219 | while (aLong < longsEnd) 220 | { 221 | if (*aLong != *bLong) 222 | return false; 223 | 224 | aLong++; 225 | bLong++; 226 | } 227 | 228 | var aChar = (char*)aLong; 229 | var bChar = (char*)bLong; 230 | 231 | switch (remainder) 232 | { 233 | case 1: 234 | return aChar[0] == bChar[0]; 235 | case 2: 236 | return aChar[0] == bChar[0] 237 | && aChar[1] == bChar[1]; 238 | case 3: 239 | return aChar[0] == bChar[0] 240 | && aChar[1] == bChar[1] 241 | && aChar[2] == bChar[2]; 242 | default: 243 | return true; 244 | } 245 | } 246 | } 247 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Performance Types 2 | 3 | [![NuGet version](https://badge.fury.io/nu/PerformanceTypes.svg)](http://badge.fury.io/nu/PerformanceTypes) 4 | [![Build status](https://ci.appveyor.com/api/projects/status/ielvdv17v6qh2py3?svg=true)](https://ci.appveyor.com/project/bretcope/performancetypes) 5 | 6 | This library is a small collection of specialized helper types which primarily focus on reducing allocations compared to BCL alternatives. __This is NOT a general-purpose library__. If you are not concerned with managed heap allocations, you will likely be better off sticking to BCL-provided types and methods. 7 | 8 | - [StopwatchStruct](#stopwatchstruct): A value-type implementation of Stopwatch for benchmarking without allocations. 9 | - [ReusableStream](#reusablestream): A [Stream](https://msdn.microsoft.com/en-us/library/system.io.stream(v=vs.110).aspx) implementation which reads from a byte array, and: 10 | - Allows the underlying data array to be swapped out without allocating a new wrapper stream. 11 | - Integrates with [StringSet](#stringset) to reduce allocations when reading strings from a byte stream. 12 | - Provides helper methods similar to [BinaryReader](https://msdn.microsoft.com/en-us/library/system.io.binaryreader(v=vs.110).aspx) and [BinaryWriter](https://msdn.microsoft.com/en-us/library/system.io.binarywriter(v=vs.110).aspx). 13 | - Provides several unsafe methods for reading and writing from byte pointers. 14 | - [StringSet](#stringset): A specialized hash set for strings which allows lookups by hash code. 15 | - [StringHash](#stringhash) 16 | - [UnsafeStringComparer](#unsafestringcomparer): A set of string-comparison methods where one or more operands are a char array or pointer. 17 | - [UnsafeMd5](#unsafemd5): An [MD5](https://en.wikipedia.org/wiki/MD5) hashing implementation which performs zero allocations. 18 | - Unsafe: a small collection of unsafe static utility methods. 19 | - [MemoryCopy](#memorycopy): A quick alternative to `memcpy` for small data buffers (~400 bytes or less). 20 | - [ToHexString](#tohexstring): Returns the hexadecimal representation of an unmanaged buffer. 21 | 22 | ## StopwatchStruct 23 | 24 | [StopwatchStruct](https://github.com/bretcope/PerformanceTypes/blob/master/PerformanceTypes/StopwatchStruct.cs) is a partial re-implementation of the [Stopwatch](https://msdn.microsoft.com/en-us/library/system.diagnostics.stopwatch) class with methods for `Start()`, `Stop()`, and `GetElapsedMilliseconds()` plus an `Elapsed` property. 25 | 26 | > This struct will only work on Windows because it relies on calls to QueryPerformanceCounter for high-resolution time measurements. 27 | 28 | The main advantage over the Stopwatch class is that StopwatchStruct is a value type, and therefore can be allocated on the stack, where it won't incur any garbage collection 29 | 30 | ```csharp 31 | var sw = new StopwatchStruct(); 32 | sw.Start(); 33 | DoSomething(); 34 | sw.Stop(); 35 | 36 | double ms = sw.GetElapsedMilliseconds(); 37 | // or 38 | TimeSpan elapsed = sw.Elapsed; 39 | ``` 40 | 41 | > StopwatchStruct is intended to be started and stopped within a single method. It's generally not recommended to pass the struct to other methods or return it unless you understand all the implications of pass-by-value for a mutable struct. 42 | 43 | ## ReusableStream 44 | 45 | The primary use case for ReusableStream is when you want to read or write binary data to a byte array. A [MemoryStream](https://msdn.microsoft.com/en-us/library/system.io.memorystream(v=vs.110).aspx) wrapped in a [BinaryReader](https://msdn.microsoft.com/en-us/library/system.io.binaryreader(v=vs.110).aspx) or [BinaryWriter](https://msdn.microsoft.com/en-us/library/system.io.binarywriter(v=vs.110).aspx) works well for this most use cases. However, those options don't allow you to swap out the underlying data source. If you want to read or write to a new buffer, you need to allocate new wrappers. They also make it difficult to reset the stream or read or write from pointers. 46 | 47 | > Most methods on this class rely on the native endianness of the system. A stream constructed on a big-endian system will not be compatible with one constructed on a little-endian system. 48 | 49 | ### Construction 50 | 51 | ```csharp 52 | // auto-create the underlying data array 53 | var stream = new ReusableStream(initialCapacity); 54 | 55 | // use a pre-created underlying data array 56 | var stream = new ReusableStream(buffer, startIndex, readableLength); 57 | 58 | // create the stream without underlying data - MUST call ReplaceData before using stream 59 | var stream = new ReusableStream(); 60 | stream.ReplaceData(buffer, startIndex, readableLength); 61 | ``` 62 | 63 | ### Read/Write 64 | 65 | ```csharp 66 | // helper methods exist for reading and writing primitives 67 | stream.Write(17); 68 | stream.ReadInt32(); 69 | 70 | // reading into a managed buffer 71 | byte[] bytes = new byte[16]; 72 | int bytesRead = stream.Read(bytes, 0, bytes.Length); 73 | 74 | // reading into an unmanaged buffer 75 | byte* bytes = stackalloc byte[16]; 76 | int bytesRead = stream.Read(bytes, 16); 77 | 78 | // writing from a buffer 79 | stream.Write(buffer, 0, buffer.Length); 80 | stream.Write(bufferPtr, bufferLength); 81 | ``` 82 | 83 | ### Resetting the Stream 84 | 85 | There are three forms of resetting. 86 | 87 | 1. __Replace the underlying data__ (by calling `ReplaceData()`): resets everything about the stream including length and position. 88 | 2. __Reset for reading__ (`ResetForReading()`): resets the position of the stream back to the initial offset, but does not adjust the length. Useful if you want to re-read all data in the stream. Equivalent to calling `Seek(0, SeekOrigin.Begin)`. 89 | 3. __Reset for writing__ (`ResetForWriting()`). Resets the position and length of the stream. Useful if you don't care about the underlying data anymore and want to write over it. 90 | 91 | ### Strings 92 | 93 | Strings can be written or read using any encoding. The default encoding is UTF8. You must specify (via the second parameter) whether the string should be considered nullable or not. 94 | 95 | ```csharp 96 | stream.DefaultEncoding = Encoding.UTF32; // change the default encoding 97 | 98 | var nullableString = stream.ReadString(true); // read nullable string 99 | var nonNullableString = stream.ReadString(false); // read non-nullable string 100 | 101 | stream.WriteString(nullableString, true); 102 | stream.WriteString(nonNullableString, false); 103 | 104 | // read/write a single string using a different encoding 105 | var str = stream.ReadString(true, Encoding.Unicode); 106 | stream.WriteString(str, true, Encoding.Unicode); 107 | ``` 108 | 109 | #### Interning 110 | 111 | When deserializing data, it is common to read the same strings over and over. In some cases, it may be preferable to reuse pre-allocated string objects rather than allocating a new string every time. For this, ReusableStream supports integrating with [StringSet](#stringset). 112 | 113 | ```csharp 114 | var set = new StringSet(initialCapacity); 115 | 116 | // add the strings you expect to see 117 | var testString = "test"; 118 | set.Add(testString); 119 | set.Add("cat"); 120 | set.Add("dog"); 121 | ... 122 | 123 | // attach the set to the stream 124 | stream.StringSet = set; 125 | 126 | // create a new options struct 127 | var options = new StringSetOptions(); 128 | 129 | /* 130 | For any string whose encoded size (in bytes) is less than or equal to this value, a lookup in 131 | StringSet will be performed before allocating a new string. If the string already exists in 132 | StringSet, then no allocation occurs. If it does not exist in StringSet, or if its encoded size 133 | is larger than this value, then a new string is allocated. 134 | 135 | For performance reasons, it is recommended to use a small value, such as 256, or less. Use zero 136 | to disable StringSet lookups altogether. 137 | */ 138 | options.MaxEncodedSizeToLookupInSet = 40; 139 | 140 | // make sure StringSet is non-null before calling this 141 | stream.SetDefaultStringOptions(options); 142 | 143 | // write the test string 144 | var originalPosition = stream.Position; 145 | stream.WriteString(testString, true); 146 | 147 | // read it back 148 | stream.Seek(originalPosition, SeekOrigin.Begin); 149 | var readString = stream.ReadString(true); 150 | 151 | // verify that we got the exact same string object 152 | object.ReferenceEquals(testString, readString); // true 153 | ``` 154 | 155 | You can override the default StringSetOptions on each call to ReadString: 156 | 157 | ```csharp 158 | var options = new StringSetOptions(); 159 | options.MaxEncodedSizeToLookupInSet = 0; // disable StringSet 160 | 161 | var newString = stream.ReadString(true, setOptions: options); 162 | ``` 163 | 164 | #### Auto-Interning 165 | 166 | If your data is coming from a trusted source, you may elect to automatically add new strings to the StringSet. This is means every time you read a string whose encoded size is `MaxEncodedSizeToLookupInSet` or less, if it doesn't already exist in the StringSet, it will be added. 167 | 168 | This is a bit dangerous because strings are _never_ purged from StringSet. The strings will never be garbage collected unless the StringSet itself is garbage collected. A user could send you millions of unique strings, causing unbounded memory growth. 169 | 170 | For this reason, the option is named `PerformDangerousAutoAddToSet`. Make sure you understand the consequences before enabling it. 171 | 172 | ```csharp 173 | var options = new StringSetOptions(); 174 | options.MaxEncodedSizeToLookupInSet = 40; 175 | options.PerformDangerousAutoAddToSet = true; 176 | ``` 177 | 178 | ## StringSet 179 | 180 | [StringSet](https://github.com/bretcope/PerformanceTypes/blob/master/PerformanceTypes/StringSet.cs) is a specialized [HashSet](https://msdn.microsoft.com/en-us/library/bb359438%28v=vs.110%29.aspx) for strings. Its primary use case is as an intern pool for parsers because it allows you to extract a substring from char array without re-allocating if that substring has been seen before. 181 | 182 | All methods on StringSet are thread-safe. `Add*` methods uses locking only when an existing match is not found. The get/search related methods were carefully designed to be thread-safe without requiring locking or spinning. 183 | 184 | There are no "remove" methods. Once a string has been added, it remains in the set until the set itself is garbage collected. 185 | 186 | ### Initializing 187 | 188 | ```csharp 189 | var set = new StringSet(INITIAL_SIZE); 190 | ``` 191 | 192 | > You must provide an explicit initial size. The set will double in size every time the current size is exceeded. 193 | 194 | ### Adding Strings 195 | 196 | If you already have the string allocated, you can add it directly: 197 | 198 | ```csharp 199 | set.Add(myString); 200 | ``` 201 | 202 | If you have a char array buffer, you can add a string to the set by providing the buffer, an offset, and the length of the string. Additionally, there is an `out string str` parameter which is the resulting string. A new string will only be allocated if it does not already exist in the set. 203 | 204 | ```csharp 205 | string result; 206 | set.Add(myCharArray, start, length, out result); 207 | ``` 208 | 209 | > All overloads of `Add()` return a `bool` which is true if the string was added to the set, and false if the string already existed. 210 | 211 | ### Searching for Strings in the Set 212 | 213 | StringSet provides two different ways to check if a string already exists in the set (without adding it). 214 | 215 | The first option is to pass in a char array: 216 | 217 | ```csharp 218 | var str = set.GetExistingString(charArray, start, length); 219 | // if the string did not exist in the set, str will be null 220 | ``` 221 | 222 | The second is to search for the string by hash code. 223 | 224 | ```csharp 225 | var hash = StringHash.GetHash(str); 226 | var cursor = set.GetSearchCursor(hash); 227 | while (cursor.MightHaveMore) 228 | { 229 | if (str == cursor.NextString()) 230 | return true; 231 | } 232 | 233 | return false 234 | ``` 235 | 236 | The cursor will only iterate over strings that were present in the set at the time the cursor was created. 237 | 238 | > Note that `cursor.MightHaveMore` is exactly what it sounds like. `NextString()` might still return null even if `MightHaveMore` is true. Once `MightHaveMore` becomes false, `NextString()` will _always_ return null. 239 | 240 | Although the cursor does maintain a reference to a pre-existing array, the cursor itself is a struct, so performing searches does not incur any heap allocations. 241 | 242 | ### StringHash 243 | 244 | [StringHash](https://github.com/bretcope/PerformanceTypes/blob/master/PerformanceTypes/StringHash.cs) is a supporting type for StringSet operations which represents the [FNV-1a hash](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function) of a string. 245 | 246 | The easiest way to generate the hash is by using one of the static `GetHash()` helper methods. 247 | 248 | ```csharp 249 | hash = StringHash.GetHash(myString); 250 | hash = StringHash.GetHash(myCharArray, start, length); 251 | ``` 252 | 253 | You can also calculate the hash by manually iterating over the characters of your string. 254 | 255 | ```csharp 256 | var hash = StringHash.Begin(); 257 | foreach (var ch in myString) 258 | { 259 | hash.Iterate(ch); 260 | } 261 | 262 | // hash is now ready for use 263 | ``` 264 | 265 | > __Never use__ `new StringHash()` or `default(StringHash)` to create the hash. It will not be initialized properly. Use `GetHash()` or `Begin()` instead. 266 | 267 | `StringSet.GetSearchCursor()` requires you to pre-calculate the hash of the string you're looking for. Several other methods of StringSet accept an optional StringHash parameter named `knownHashValue`. If you have pre-calculated the hash, you can use this parameter to save unnecessary re-calculation, but be sure you're supplying the correctly calculated hash. An incorrectly calculated hash could result in duplicate strings or make them unsearchable. 268 | 269 | ### StringSet Use Cases 270 | 271 | > __IMPORTANT__: Don't ever add arbitrary user-provided strings to a StringSet. Remember that there is no "remove" functionality. A user could intentionally or unintentionally cause your memory usage to grow, and your application to eventually crash, by sending a large volume of unique strings. 272 | 273 | StringSet is primarily intended to support parsers in a production environment where allocations matter (because of garbage collection performance). The most common use case would be to pre-allocate common strings you expect to see, and then use StringSet to avoid allocating a new string every time you parse a common value. 274 | 275 | For example, let's say you want to parse a semicolon-delimited list of [Stack Overflow tags](https://stackoverflow.com/tags) (e.g. `c#;java;c++;sql;`). The naïve implementation would be: 276 | 277 | ```csharp 278 | var tags = tagListString.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); 279 | ``` 280 | 281 | There's nothing wrong with that implementation if allocations aren't a big deal for your application. On the other hand, if they _are_ a big deal, here's how you could re-write this parser using StringSet: 282 | 283 | ```csharp 284 | // StringSet initialization code you would do once 285 | var tagsWeCareExpect = new[] { "c#", "java", "c++" }; 286 | var set = new StringSet(tagsWeCareExpect.Length + 10); 287 | foreach (var tag in tagsWeCareExpect) 288 | { 289 | set.Add(tag); 290 | } 291 | 292 | // parsing code 293 | var start = 0; 294 | for (var i = 0; i < buffer.Length; i++) 295 | { 296 | var ch = buffer[i]; 297 | 298 | if (ch == ';') 299 | { 300 | var len = i - start; 301 | var str = set.GetExistingString(buffer, start, len); 302 | if (str == null) 303 | { 304 | // Was not a tag we expected, so we have to allocate a new string. 305 | // We don't want to add it to the set because this is user input. 306 | str = new string(buffer, start, len); 307 | } 308 | 309 | // todo - actually do something with this string 310 | 311 | start = i + 1; 312 | } 313 | 314 | // todo - handle if the list isn't semi-colon terminated 315 | } 316 | ``` 317 | 318 | Obviously that's a lot more code, but it may be worth it for high-volume parsers. 319 | 320 | ## UnsafeStringComparer 321 | 322 | [UnsafeStringComparer](https://github.com/bretcope/PerformanceTypes/blob/master/PerformanceTypes/UnsafeStringComparer.cs) is a static helper class which helps you perform fast string equality comparisons when one operand is a string and the other is a character buffer. It is called "Unsafe..." because it uses [unsafe](https://msdn.microsoft.com/en-us/library/chfa2zb8.aspx) code for optimizations and some methods accept pointers. 323 | 324 | For strings with seven or fewer characters, they are compared one character at a time. For strings with eight or more characters, UnsafeStringComparer will switch to comparing four characters at a time via 64-bit integers, which can result in an almost 4x performance improvement. 325 | 326 | > SIMD would likely provide additional performance improvements, but C# does not appear to provide a way to use SIMD with unmanaged pointers. It would have to be done in a native dll, which would make this library less portable. 327 | 328 | There is only one method with four public overloads: 329 | 330 | ```csharp 331 | bool AreEqual(string str, char[] buffer) 332 | ``` 333 | 334 | > Compares all characters from `str` with all characters in `buffer`. 335 | 336 | ```csharp 337 | bool AreEqual(string str, char[] buffer, int start, int length) 338 | ``` 339 | 340 | > Compares all characters from `str` with the characters in buffer beginning at the `start` index, and for `length` characters. If `length != str.Length`, or if `start + length > buffer.Length`, the return value will always be false. 341 | 342 | ```csharp 343 | bool AreEqual(string str, char* buffer, int length) 344 | ``` 345 | 346 | > Compares all the characters from `str` with a character buffer pointed to by `buffer` for `length` characters. If `length != str.Length`, the return value will always be false. It is up to you to ensure that the buffer has at least `length` characters remaining, or this method may read from memory outside your buffer. 347 | 348 | ```csharp 349 | bool AreEqual(char* aPtr, char* bPtr, int length) 350 | ``` 351 | 352 | > Compares characters from two buffers pointed at by `aPtr` and `bPtr` for `length` characters. It is up to you to ensure both buffers have at least `length` characters remaining, or this method may read from memory outside your buffers. 353 | 354 | ## UnsafeMd5 355 | 356 | The MD5 methods in the BCL allocate a byte array in order to return the hash value. The methods on UnsafeMd5 return the hash as an `Md5Digest` struct, and therefore avoid any heap allocations. This implementation will out-perform the BCL implementation for small buffer sizes (~700 bytes or less), but is a slower on larger buffers. 357 | 358 | > __Note: UnsafeMd5 will only return the accurate MD5 hash on little endian architectures (such as Intel x86).__ 359 | 360 | ```csharp 361 | Md5Digest digest; 362 | 363 | // managed interface 364 | UnsafeMd5.ComputeHash(byteArray, out digest); 365 | 366 | // unmanaged interface 367 | UnsafeMd5.ComputeHash(bytePtr, length, &digest); 368 | ``` 369 | 370 | The Md5Digest struct is 16 bytes (the size of an MD5 hash). This constant is also available via `Md5Digest.SIZE`. 371 | 372 | There are a few different ways to get the actual bytes from the digest struct. 373 | 374 | ```csharp 375 | // cast it as a byte pointer 376 | var bytesPtr = (byte*)&digest; 377 | 378 | // copy the bytes into a managed byte[] buffer starting at index 379 | digest.WriteBytes(buffer, index); 380 | 381 | // copy bytes to an unmanaged buffer (make sure at least 16 bytes are available) 382 | digest.WriteBytes(bufferPtr); 383 | 384 | // call GetBytes to simply allocate a new byte[] 385 | var bytes = digest.GetBytes(); 386 | 387 | // call ToString to get a hex representation of the hash (obviously this allocates a string) 388 | var hex = digest.ToString(); // example: 1f2cc2829f9ec439fab4f45ab54d8a82 389 | ``` 390 | 391 | Md5Digest also implements `IEquatable` for comparison purposes and offers operator overloads for `==` and `!=`. 392 | 393 | ## Unsafe 394 | 395 | A small static utilities class for unsafe operations. 396 | 397 | ### MemoryCopy 398 | 399 | This is not a general-purpose replacement for `memcpy` or methods like `Marshal.Copy`. However, it is a fast alternative for small data buffers. In my testing, it will out-perform the alternatives for buffer sizes of around 400 bytes or less. 400 | 401 | ```csharp 402 | Unsafe.MemoryCopy(srcPtr, destPtr, bytesCount); 403 | ``` 404 | 405 | ### ToHexString 406 | 407 | Takes an unmanaged byte pointer and length (in bytes), and returns a hexadecimal representation of the data. The string itself is the only heap allocation this method makes. There are no intermediate objects. 408 | 409 | ```csharp 410 | var bytes = stackalloc byte[2]; 411 | bytes[0] = 0xa5; 412 | bytes[1] = 0xf7; 413 | 414 | var str = ToHexString(bytes, 2); // "a5f7" 415 | ``` 416 | 417 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # See http://www.appveyor.com/docs/appveyor-yml for reference 2 | 3 | #---------------------------------# 4 | # general configuration # 5 | #---------------------------------# 6 | 7 | 8 | #---------------------------------# 9 | # environment configuration # 10 | #---------------------------------# 11 | 12 | os: Visual Studio 2017 13 | 14 | #---------------------------------# 15 | # build configuration # 16 | #---------------------------------# 17 | 18 | build_script: 19 | - ps: .\build.ps1 -pack 20 | 21 | #---------------------------------# 22 | # tests configuration # 23 | #---------------------------------# 24 | 25 | test: off 26 | 27 | #---------------------------------# 28 | # artifacts configuration # 29 | #---------------------------------# 30 | 31 | artifacts: 32 | - path: artifacts/* 33 | 34 | #---------------------------------# 35 | # deployment configuration # 36 | #---------------------------------# 37 | 38 | deploy: 39 | 40 | - provider: GitHub 41 | release: $(APPVEYOR_REPO_TAG_NAME) 42 | auth_token: 43 | secure: R4+VK7DPGDBh/IvVDn06bAqBr/QKFS5eB9OFQZLzKsapKq1QJ+ytmHx71v7mV9oO 44 | artifact: artifacts/* 45 | draft: false 46 | prerelease: false 47 | on: 48 | appveyor_repo_tag: true 49 | 50 | # MyGet 51 | - provider: NuGet 52 | server: https://www.myget.org/F/bretcope/api/v2/package 53 | symbol_server: https://www.myget.org/F/bretcope/symbols/api/v2/package 54 | api_key: 55 | secure: 4OhfGM3pJQHTs5OYskc448dwXt8DqPec7vdyK+G+AS5v0m8pSxyP30UoZyTanDcz 56 | artifact: /artifacts/.*\.nupkg/ 57 | skip_symbols: false 58 | on: 59 | branch: master 60 | 61 | # NuGet.org 62 | - provider: NuGet 63 | api_key: 64 | secure: NiH62CvEa6ztPGnR9glvKexCL5sJStB62JcJ/8CcVJJAj7rkCzZvfN87E5xPpNd0 65 | artifact: /artifacts/.*\.nupkg/ 66 | skip_symbols: false 67 | on: 68 | appveyor_repo_tag: true 69 | 70 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | powershell -NoProfile -NoLogo -ExecutionPolicy UnRestricted %~dp0build.ps1 %* 3 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | # Specifies a pre-release build number. This is ignored if -Stable is used. 3 | [int] $BuildNum, 4 | # Indicates this is a Stable release. If set, the nuget version number is left unaltered. 5 | [switch] $Stable, 6 | # The configuration target for the build (e.g. Debug or Release) 7 | [string] $Configuration = "Release", 8 | # Skips tests. 9 | [switch] $SkipTests, 10 | # Creates nuget packages. 11 | [switch] $Pack, 12 | # The verbosity level. Options: quiet, minimal, normal, detailed, or diagnostic. Defaults to minimal. 13 | [string] $Verbosity = "minimal", 14 | # Enables Powershell debugging output 15 | [switch] $PsDebug 16 | ) 17 | 18 | $AppVeyor = $env:APPVEYOR -eq 'true' 19 | 20 | function Main() 21 | { 22 | # project config 23 | $buildProject = 'PerformanceTypes.sln' 24 | $nugetProject = 'PerformanceTypes\PerformanceTypes.csproj' 25 | $testProject = 'PerformanceTypes.Tests\PerformanceTypes.Tests.csproj' 26 | 27 | # build script config 28 | if ($PsDebug) 29 | { 30 | $DebugPreference = "Continue" 31 | } 32 | 33 | if ($AppVeyor) 34 | { 35 | Write-Debug 'Using AppVeyor build mode. -BuildNum and -Stable parameters are ignored.' 36 | $BuildNum = [int]::Parse("$env:APPVEYOR_BUILD_NUMBER") 37 | $Stable = [bool]::Parse("$env:APPVEYOR_REPO_TAG") 38 | } 39 | 40 | 41 | $version = GetVersion $nugetProject $BuildNum $Stable 42 | 43 | # Restore 44 | Write-Host 45 | Write-Host -BackgroundColor "Cyan" -ForegroundColor "Black" "Restoring $buildProject" 46 | ExecuteDotNet $buildProject 'restore' 47 | 48 | # Build 49 | Write-Host 50 | Write-Host -BackgroundColor "Cyan" -ForegroundColor "Black" "Building $buildProject" 51 | ExecuteDotNet $buildProject 'build' 52 | 53 | # Test 54 | if ($testProject -and $SkipTests -ne $true) 55 | { 56 | Write-Host 57 | Write-Host -BackgroundColor "Cyan" -ForegroundColor "Black" "Running Tests" 58 | ExecuteDotNet $testProject 'test' 59 | } 60 | 61 | # Nuget Pack 62 | if ($Pack) 63 | { 64 | Write-Host 65 | Write-Host -BackgroundColor "Cyan" -ForegroundColor "Black" "Creating NuGet Package" 66 | Write-Host -BackgroundColor "Cyan" -ForegroundColor "Black" "Version: $version" 67 | ExecuteDotNet $nugetProject 'pack' $version 68 | } 69 | 70 | Write-Host 71 | Write-Host -ForegroundColor "Green" "Build Complete" 72 | Write-Host 73 | } 74 | 75 | function ExecuteDotNet([string] $project, [string] $cmd, [string] $version) 76 | { 77 | [string[]] $dotnetArgs = $cmd, "`"$project`"" 78 | 79 | switch ($cmd) 80 | { 81 | "restore" { } 82 | "build" { 83 | $dotnetArgs += "-c=$Configuration" 84 | $dotnetArgs += "--no-restore" 85 | } 86 | "test" { 87 | $dotnetArgs += "-c=$Configuration" 88 | $dotnetArgs += "--no-restore" 89 | } 90 | "pack" { 91 | $dotnetArgs += "-c=$Configuration" 92 | $dotnetArgs += "--no-restore" 93 | $dotnetArgs += "/p:IncludeSymbols=true" 94 | $dotnetArgs += "/p:PackageVersion=$version" 95 | $dotnetArgs += "/p:PackageOutputPath=`"$(Join-Path $PSScriptRoot 'artifacts')`"" 96 | } 97 | } 98 | 99 | if ($AppVeyor) 100 | { 101 | # $dotnetArgs += "/l:`"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll`"" 102 | } 103 | 104 | $dotnetArgs += "-v=$Verbosity" 105 | 106 | Write-Host -ForegroundColor "Cyan" "dotnet $dotnetargs" 107 | & dotnet $dotnetargs 108 | 109 | if ($LASTEXITCODE -ne 0) 110 | { 111 | Write-Host 112 | Write-Host -ForegroundColor "Red" 'Build Failed' 113 | Write-Host 114 | 115 | Exit 1 116 | } 117 | } 118 | 119 | function GetVersion([string] $nugetProject, [int] $buildNum, [bool] $stable) 120 | { 121 | $fileName = Join-Path -Path (Get-Location) -ChildPath $nugetProject 122 | $csXml = New-Object XML 123 | $csXml.Load($fileName) 124 | 125 | $packageVersionNode = GetPackageVersionNode $csXml 126 | $version = $packageVersionNode.InnerText 127 | 128 | $versionGroups = GetVersionGroups $version 129 | 130 | if (!$stable) 131 | { 132 | $version = GeneratePrereleaseVersion $versionGroups $buildNum 133 | } 134 | 135 | Write-Output -NoEnumerate $version 136 | } 137 | 138 | function GetPackageVersionNode ([xml] $csXml) 139 | { 140 | # find the PackageVersion element 141 | $nodes = $csXml.GetElementsByTagName("PackageVersion") 142 | if ($nodes.Count -eq 0) 143 | { 144 | Write-Error "$csproj does not contain a element." 145 | Exit 1 146 | } 147 | 148 | if ($nodes.Count -gt 1) 149 | { 150 | Write-Error "$csproj contains more than one instance of " 151 | Exit 1 152 | } 153 | 154 | Write-Output -NoEnumerate $nodes[0] 155 | } 156 | 157 | function GetVersionGroups ([string] $version) 158 | { 159 | # validate the package version and extract major version 160 | $versionMatch = [Regex]::Match($version, '^(?\d+)\.(?\d+)\.(?\d+)(?-\S+)?$') 161 | if (!$versionMatch.Success) 162 | { 163 | Write-Error "Invalid PackageVersion: $version. It must follow the MAJOR.MINOR.PATCH format." 164 | Exit 1 165 | } 166 | 167 | Write-Output -NoEnumerate $versionMatch.Groups 168 | } 169 | 170 | function GeneratePrereleaseVersion ([System.Text.RegularExpressions.GroupCollection] $versionGroups, [int] $buildNum) 171 | { 172 | $version = $versionGroups["Major"].Value + "." + $versionGroups["Minor"].Value + "." 173 | 174 | if ($versionGroups["PreRelease"].Success) 175 | { 176 | # the version is already marked as a pre-release, so we don't need to increment the patch number 177 | $version += $versionGroups["Patch"].Value + $versionGroups["PreRelease"].Value 178 | } 179 | else 180 | { 181 | $patch = [int]::Parse($versionGroups["Patch"]) + 1 182 | $version += "$patch-unstable" 183 | } 184 | 185 | $version += "-$buildNum" 186 | 187 | Write-Output -NoEnumerate $version 188 | } 189 | 190 | Main 191 | --------------------------------------------------------------------------------