Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nuid #360

Merged
merged 30 commits into from
Jan 28, 2020
Merged

Nuid #360

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6f48311
Add MicroBenchmarks
jasper-d Jan 12, 2020
dceef0d
Make internals visible to test and benchmarks
jasper-d Jan 12, 2020
a2d7541
Add baseline benchmarks
jasper-d Jan 12, 2020
5155714
Implement Nuid
jasper-d Jan 12, 2020
e949c6b
Fix MicroBenchmarks on Linux
jasper-d Jan 13, 2020
955254d
Improve performance by ~40%
jasper-d Jan 13, 2020
bcea8d9
Use base64 encoding and optimize div and mod
jasper-d Jan 15, 2020
b075eec
Fix test arrangement
jasper-d Jan 15, 2020
951d2aa
Use 0 as sequential in benchmarks
jasper-d Jan 26, 2020
89fe7b2
Use Nuid as inbox subject
jasper-d Jan 26, 2020
25597a1
Mark NUID as obsolete
jasper-d Jan 26, 2020
006f2e5
Fix project and solution files after rebasing
jasper-d Jan 26, 2020
e3405d0
Always initialize System.Random w/ seed
jasper-d Jan 26, 2020
01cc5cb
Make string comparision method explicit in test
jasper-d Jan 26, 2020
3ac15bf
Fix test after change in e3405d0
jasper-d Jan 26, 2020
f17641a
Fix MicroBenchmarks
jasper-d Jan 26, 2020
8a50ee5
Test different synchronization primitives
jasper-d Jan 26, 2020
bd2abf3
Remove slower implementations of GetNext
jasper-d Jan 26, 2020
90aef28
Remove unnecessary locks
jasper-d Jan 26, 2020
e8fe4d2
Cleanup benchmarks
jasper-d Jan 26, 2020
66ce7c1
Remove unused field from NuidBenchmark
jasper-d Jan 26, 2020
ec7253a
Suppress obsolete warnings in benchmarks and tests
jasper-d Jan 26, 2020
e51e42d
Add LF at EOF
jasper-d Jan 26, 2020
4f11c79
Fix comment
jasper-d Jan 26, 2020
05da0a3
Add license headers
jasper-d Jan 27, 2020
a8b925e
Use URL and Filename safe digits
jasper-d Jan 27, 2020
42eb8e8
Fix tests cf. a8b925e
jasper-d Jan 27, 2020
698acc9
Remove non-existent configuration in condition
jasper-d Jan 27, 2020
55ca475
Remove cast and use unsigned integers
jasper-d Jan 28, 2020
27f35bb
Use lock statement to ensure monitor is exited
jasper-d Jan 28, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,6 @@ paket-files/
# JetBrains Rider
.idea/
*.sln.iml

# Benchmark artifacts
**/BenchmarkDotNet.Artifacts/
7 changes: 7 additions & 0 deletions src/Benchmarks/MicroBenchmarks/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project>
<PropertyGroup>
<!-- We need to override these, otherwise benchmarks won't work :/ -->
<TargetFrameworks></TargetFrameworks>
<TargetFramework></TargetFramework>
</PropertyGroup>
</Project>
25 changes: 25 additions & 0 deletions src/Benchmarks/MicroBenchmarks/MicroBenchmarks.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework Condition="$(OS) != 'Windows_NT'">netcoreapp3.1</TargetFramework>
<TargetFrameworks Condition="$(OS) == 'Windows_NT'">netcoreapp3.1;net462</TargetFrameworks>
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>

<PropertyGroup Condition="$(Configuration) == 'Release'">
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\..\NATS.Client.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\NATS.Client\NATS.Client.csproj" />
</ItemGroup>

</Project>
47 changes: 47 additions & 0 deletions src/Benchmarks/MicroBenchmarks/NuidBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2020 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;
using NATS.Client;
using NATS.Client.Internals;

namespace MicroBenchmarks
{
[DisassemblyDiagnoser(printAsm: true, printSource: true)]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[MemoryDiagnoser]
[MarkdownExporterAttribute.GitHub]
[SimpleJob(RuntimeMoniker.Net462)]
[SimpleJob(RuntimeMoniker.NetCoreApp31)]
public class NuidBenchmark
{
#pragma warning disable CS0618
private readonly NUID _nuid = NUID.Instance;
#pragma warning restore CS0618
private readonly Nuid _newNuid = new Nuid(null, 0, 1);

public NuidBenchmark()
{
_nuid.Seq = 0;
}

[BenchmarkCategory("NextNuid")]
[Benchmark(Baseline = true)]
public string NUIDNext() => _nuid.Next;

[BenchmarkCategory("NextNuid"), Benchmark]
public string NextNuid() => _newNuid.GetNext();
}
}
25 changes: 25 additions & 0 deletions src/Benchmarks/MicroBenchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2020 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using BenchmarkDotNet.Running;

namespace MicroBenchmarks
{
public class Program
{
public static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<NuidBenchmark>();
}
}
}
21 changes: 3 additions & 18 deletions src/NATS.Client/Conn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public class Connection : IConnection, IDisposable
// .NET 4.0.
private readonly object mu = new Object();

private Random r = null;
private readonly Nuid _nuid = new Nuid();

Options opts = new Options();

Expand Down Expand Up @@ -3191,23 +3191,8 @@ public Task<Msg> RequestAsync(string subject, byte[] data, int offset, int count
/// <returns>A unique inbox string.</returns>
public string NewInbox()
{
var prefix = opts.CustomInboxPrefix ?? IC.inboxPrefix;

if (!opts.UseOldRequestStyle)
{
return prefix + Guid.NewGuid().ToString("N");
}
else
{
if (r == null)
r = new Random(Guid.NewGuid().GetHashCode());

byte[] buf = new byte[13];

r.NextBytes(buf);

return prefix + BitConverter.ToString(buf).Replace("-","");
}
var prefix = opts.customInboxPrefix ?? IC.inboxPrefix;
return prefix + _nuid.GetNext();
}

internal void sendSubscriptionMessage(AsyncSubscription s)
Expand Down
174 changes: 174 additions & 0 deletions src/NATS.Client/Internals/Nuid.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Copyright 2020 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Security.Cryptography;
using System.Threading;

namespace NATS.Client.Internals
{
internal sealed class Nuid
{
private const uint PREFIX_LENGTH = 12;
private const uint SEQUENTIAL_LENGTH = 10;
private const uint NUID_LENGTH = PREFIX_LENGTH + SEQUENTIAL_LENGTH;
private const int MIN_INCREMENT = 33;
private const int MAX_INCREMENT = 333;
private const ulong MAX_SEQUENTIAL = 0x1000_0000_0000_0000; //64^10
private const int BASE = 64;

private static readonly byte[] _digits = new byte[BASE]{
(byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', (byte)'H',
(byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', (byte)'O', (byte)'P',
(byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', (byte)'V', (byte)'W', (byte)'X',
(byte)'Y', (byte)'Z', (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f',
(byte)'g', (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
(byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', (byte)'v',
(byte)'w', (byte)'x', (byte)'y', (byte)'z', (byte)'0', (byte)'1', (byte)'2', (byte)'3',
(byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'-', (byte)'_',
};

private readonly object _nuidLock = new object();

private readonly Random _rng;
private readonly RandomNumberGenerator _cryptoRng;

private byte[] _prefix = new byte[PREFIX_LENGTH];
private uint _increment;
private ulong _sequential;

/// <summary>
/// Initializes a new instance of <see cref="Nuid"/>.
/// </summary>
/// <remarks>
/// This constructor is intended to be used from unit tests and
/// benchmarks only. For production use use <see cref="Nuid()"/> instead.
/// </remarks>
/// <param name="rng">A cryptographically strong random number generator.</param>
/// <param name="sequential">The initial sequential.</param>
/// <param name="increment">The initial increment.</param>
internal Nuid(RandomNumberGenerator rng = null, ulong? sequential = null, uint? increment = null)
{
if (rng is null)
_cryptoRng = RandomNumberGenerator.Create();
else
_cryptoRng = rng;

// Instantiating System.Random multiple times in quick succession without a
// proper seed may result in instances that yield identical sequences on .NET FX.
// See https://docs.microsoft.com/en-us/dotnet/api/system.random?view=netframework-4.8#instantiating-the-random-number-generator
// and https://docs.microsoft.com/en-us/dotnet/api/system.random?view=netframework-4.8#avoiding-multiple-instantiations
var seedBytes = new byte[4];
_cryptoRng.GetBytes(seedBytes);
_rng = new Random(BitConverter.ToInt32(seedBytes, 0));

if (sequential is null)
_sequential = GetSequential();
else
_sequential = sequential.Value;

if (increment is null)
_increment = GetIncrement();
else
_increment = increment.Value;

SetPrefix();
}

/// <summary>
/// Initializes a new instance of <see cref="Nuid"/>.
/// </summary>
internal Nuid() : this(null) {}

/// <summary>
/// Returns a random Nuid string.
/// </summary>
/// <remarks>
/// A Nuid is a 132 bit pseudo-random integer encoded as a base64 string
/// </remarks>
/// <returns>The Nuid</returns>
internal string GetNext()
{
var nuidBuffer = new char[NUID_LENGTH];
var sequential = 0UL;

lock (_nuidLock)
{
sequential = _sequential += _increment;
if (_sequential >= MAX_SEQUENTIAL)
{
SetPrefix();
sequential = _sequential = GetSequential();
_increment = GetIncrement();
}

// For small arrays this is way faster than Array.Copy and still faster than Buffer.BlockCopy
for (var i = 0; i < PREFIX_LENGTH; i++)
{
nuidBuffer[i] = (char)_prefix[i];
}
}

for (var i = PREFIX_LENGTH; i < NUID_LENGTH; i++)
{
// We operate on unsigned integers and BASE is a power of two
// therefore we can optimize sequential % BASE to sequential & (BASE - 1)
nuidBuffer[i] = (char)_digits[sequential & (BASE - 1)];
// BASE is 64 = 2^6 and sequential >= 0
// therefore we can optimize sequential / BASE to sequential >> 6
sequential >>= 6;
}

return new string(nuidBuffer);
}

private uint GetIncrement()
{
return (uint)_rng.Next(MIN_INCREMENT, MAX_INCREMENT);
}

private ulong GetSequential()
{
var randomBytes = new byte[8];

_rng.NextBytes(randomBytes);

var sequential = BitConverter.ToUInt64(randomBytes, 0);

// NOTE: Originally we used the following algorithm to create a random long:
// https://stackoverflow.com/a/13095144
// Here, the uRange is const though, because it is always MAX_SEQUENTIAL - 0,
// so the condition can be reduced to:
// sequential > ulong.MaxValue - ((ulong.MaxValue % MAX_SEQUENTIAL) + 1) % MAX_SEQUENTIAL
// The right hand side of the comparision happens to be const too now and can be folded to:
// 18446744073709551615 which happens to be equal to ulong.MaxValue, hence the condition will
// never be true and we can omit the do-while loop entirely.

return sequential % MAX_SEQUENTIAL;
}

private void SetPrefix()
{
var randomBytes = new byte[PREFIX_LENGTH];

_cryptoRng.GetBytes(randomBytes);

for(var i = 0; i < randomBytes.Length; i++)
{
// We operate on unsigned integers and BASE is a power of two
// therefore we can optimize randomBytes[i] % BASE to randomBytes[i] & (BASE - 1)
_prefix[i] = _digits[randomBytes[i] & (BASE - 1)];
}
}
}
}
4 changes: 3 additions & 1 deletion src/NATS.Client/NATS.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
<PackageReleaseNotes>https://github.com/nats-io/nats.net/releases</PackageReleaseNotes>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageTags>CNCF NATS Messaging Cloud Publish Subscribe PubSub</PackageTags>
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>

<PropertyGroup Condition="$(Configuration) == 'Release'">
Expand All @@ -20,7 +22,7 @@
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\NATS.Client.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

<ItemGroup>
<None Include="package-icon.png">
<Pack>True</Pack>
Expand Down
1 change: 1 addition & 0 deletions src/NATS.Client/NUID.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ namespace NATS.Client
/// that is started at a pseudo random number and increments with a pseudo-random increment.
/// Total is 22 bytes of base 36 ascii text.
/// </summary>
[Obsolete("NATS.Client.NUID is deprecated and will be removed in a future version")]
public class NUID
{
private static char[] digits =
Expand Down
13 changes: 13 additions & 0 deletions src/NATS.Client/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Runtime.CompilerServices;

// Since NATS.Client is signed, friends must be signed too.
// We use the same SNK for simplicity.
// https://docs.microsoft.com/en-us/dotnet/standard/assembly/create-signed-friend
#if DEBUG
[assembly: InternalsVisibleTo("UnitTests")]
[assembly: InternalsVisibleTo("MicroBenchmarks")]

#else
[assembly: InternalsVisibleTo("UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100db7da1f2f89089327b47d26d69666fad20861f24e9acdb13965fb6c64dfee8da589b495df37a75e934ddbacb0752a42c40f3dbc79614eec9bb2a0b6741f9e2ad2876f95e74d54c23eef0063eb4efb1e7d824ee8a695b647c113c92834f04a3a83fb60f435814ddf5c4e5f66a168139c4c1b1a50a3e60c164d180e265b1f000cd")]
[assembly: InternalsVisibleTo("MicroBenchmarks, PublicKey=0024000004800000940000000602000000240000525341310004000001000100db7da1f2f89089327b47d26d69666fad20861f24e9acdb13965fb6c64dfee8da589b495df37a75e934ddbacb0752a42c40f3dbc79614eec9bb2a0b6741f9e2ad2876f95e74d54c23eef0063eb4efb1e7d824ee8a695b647c113c92834f04a3a83fb60f435814ddf5c4e5f66a168139c4c1b1a50a3e60c164d180e265b1f000cd")]
#endif
Loading