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

Implement NUID #147

Merged
merged 7 commits into from
Oct 10, 2023
Merged

Implement NUID #147

merged 7 commits into from
Oct 10, 2023

Conversation

jasper-d
Copy link
Contributor

@jasper-d jasper-d commented Oct 6, 2023

Fixes #34

| Method               | Job           | Runtime       | Mean     | Error    | StdDev   | Median   | Ratio | RatioSD | Gen0   | Allocated | Alloc Ratio |
|--------------------- |-------------- |-------------- |---------:|---------:|---------:|---------:|------:|--------:|-------:|----------:|------------:|
| TryWriteNuid         | .NET 6.0      | .NET 6.0      | 31.00 ns | 0.365 ns | 0.305 ns | 31.06 ns |  0.99 |    0.03 |      - |         - |          NA |
| NewInbox_ShortPrefix | .NET 6.0      | .NET 6.0      | 51.74 ns | 1.078 ns | 1.476 ns | 51.90 ns |  1.64 |    0.07 | 0.0612 |     128 B |          NA |
| NewInbox_LongPrefix  | .NET 6.0      | .NET 6.0      | 76.69 ns | 1.568 ns | 2.662 ns | 76.52 ns |  2.44 |    0.12 | 0.1988 |     416 B |          NA |
| TryWriteNuid         | .NET 7.0      | .NET 7.0      | 31.61 ns | 0.661 ns | 0.926 ns | 31.21 ns |  1.00 |    0.00 |      - |         - |          NA |
| NewInbox_ShortPrefix | .NET 7.0      | .NET 7.0      | 54.38 ns | 1.116 ns | 1.044 ns | 54.72 ns |  1.74 |    0.06 | 0.0612 |     128 B |          NA |
| NewInbox_LongPrefix  | .NET 7.0      | .NET 7.0      | 77.58 ns | 0.909 ns | 0.850 ns | 77.68 ns |  2.48 |    0.08 | 0.1988 |     416 B |          NA |
| TryWriteNuid         | .NET 8.0      | .NET 8.0      | 25.50 ns | 0.499 ns | 0.390 ns | 25.58 ns |  0.82 |    0.03 |      - |         - |          NA |
| NewInbox_ShortPrefix | .NET 8.0      | .NET 8.0      | 46.84 ns | 0.986 ns | 1.317 ns | 46.94 ns |  1.48 |    0.06 | 0.0612 |     128 B |          NA |
| NewInbox_LongPrefix  | .NET 8.0      | .NET 8.0      | 66.03 ns | 0.838 ns | 0.784 ns | 66.22 ns |  2.11 |    0.07 | 0.1988 |     416 B |          NA |
| TryWriteNuid         | NativeAOT 8.0 | NativeAOT 8.0 | 25.15 ns | 0.495 ns | 0.463 ns | 25.33 ns |  0.80 |    0.03 |      - |         - |          NA |
| NewInbox_ShortPrefix | NativeAOT 8.0 | NativeAOT 8.0 | 45.00 ns | 0.945 ns | 1.843 ns | 44.25 ns |  1.44 |    0.06 | 0.0612 |     128 B |          NA |
| NewInbox_LongPrefix  | NativeAOT 8.0 | NativeAOT 8.0 | 56.34 ns | 1.139 ns | 1.066 ns | 55.62 ns |  1.80 |    0.07 | 0.1989 |     416 B |          NA |

Opted to go with base-62 here to follow nuid.go. base-64 (as implemented and reviewed in #360) would avoid some bias and be about twice as fast.

@@ -61,6 +64,42 @@ public partial class NatsConnection
}
}

[SkipLocalsInit]
private static string NewInbox(ReadOnlySpan<char> prefix)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is a bit of a mess. It is rather complicated and still slow and allocates quite heavily. Allocations could be reduced by renting buffers instead of new'ing them, but that reduces throughput.

Maybe it would be possible to not operate with strings (apart from public interfaces) for inbox prefix/subjects and instead use only byte[]. But that would be a more invasive change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E.g. this alternative implementation which looks much more reasonable

    [SkipLocalsInit]
    private static string NewInbox(ReadOnlySpan<char> prefix)
    {
        Span<char> buffer = stackalloc char[22];
        NuidWriter.TryWriteNuid(buffer);

        if (prefix.IsEmpty)
        {
            return new string(buffer);
        }
        else
        {
            return $"{prefix}.{buffer}";
        }
    }

is slower but also reduces the allocations for long prefixes:

| Method               | Job           | Runtime       | Mean     | Error    | StdDev   | Ratio | RatioSD | Gen0   | Allocated | Alloc Ratio |
|--------------------- |-------------- |-------------- |---------:|---------:|---------:|------:|--------:|-------:|----------:|------------:|
| NewInbox_ShortPrefix | .NET 6.0      | .NET 6.0      | 79.85 ns | 1.617 ns | 2.370 ns |  0.91 |    0.05 | 0.0612 |     128 B |        1.00 |
| NewInbox_ShortPrefix | .NET 7.0      | .NET 7.0      | 87.93 ns | 1.789 ns | 2.732 ns |  1.00 |    0.00 | 0.0612 |     128 B |        1.00 |
| NewInbox_ShortPrefix | .NET 8.0      | .NET 8.0      | 72.40 ns | 1.353 ns | 1.266 ns |  0.82 |    0.03 | 0.0612 |     128 B |        1.00 |
| NewInbox_ShortPrefix | NativeAOT 8.0 | NativeAOT 8.0 | 66.20 ns | 1.358 ns | 1.270 ns |  0.75 |    0.03 | 0.0612 |     128 B |        1.00 |
|                      |               |               |          |          |          |       |         |        |           |             |
| NewInbox_LongPrefix  | .NET 6.0      | .NET 6.0      | 82.68 ns | 0.958 ns | 1.025 ns |  0.97 |    0.02 | 0.0994 |     208 B |        1.00 |
| NewInbox_LongPrefix  | .NET 7.0      | .NET 7.0      | 85.93 ns | 0.881 ns | 0.688 ns |  1.00 |    0.00 | 0.0994 |     208 B |        1.00 |
| NewInbox_LongPrefix  | .NET 8.0      | .NET 8.0      | 78.37 ns | 1.593 ns | 1.705 ns |  0.91 |    0.02 | 0.0994 |     208 B |        1.00 |
| NewInbox_LongPrefix  | NativeAOT 8.0 | NativeAOT 8.0 | 72.60 ns | 1.492 ns | 1.532 ns |  0.85 |    0.01 | 0.0994 |     208 B |        1.00 |

for (nuint i = PrefixLength; i < NuidLength; i++)
{
var digitIndex = (nuint)(sequential % Base);
Unsafe.Add(ref buffer[0], i) = Unsafe.Add(ref digitsPtr, digitIndex);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usage of Unsafe is necessary here to avoid range checks, both when accessing buffer and Digits.

@jasper-d jasper-d marked this pull request as ready for review October 6, 2023 19:08
private static string NewInbox(ReadOnlySpan<char> prefix)
{
Span<char> buffer = stackalloc char[64];
var separatorLength = prefix.Length > 0 ? 1u : 0u;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if support for empty prefixes is really needed, but right now nothing is preventing users from doing it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK, practice is to have a prefix so the accounts can be permissined accordingly.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwiw, Actually just realized an empty prefix might create interesting behaviours in subscription manager i.e. SubscriptionManager.IsInboxSubject() would always return true causing all subscriptions to go through an internal muxed 'inbox' subscription.

@mtmk
Copy link
Collaborator

mtmk commented Oct 10, 2023

Thanks @jasper-d looks awesome! Sorry for the delay. Started reviewing today and trying to get my head around it.

@jasper-d
Copy link
Contributor Author

Sorry for the delay.

No worries, this isn't exactly urgent.

Copy link
Collaborator

@mtmk mtmk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Nicely follows the Go implementation.

Thanks @jasper-d. I have a question below out of curiosity, but please feel free to ignore it if you don't have time. It's not a merge-stopper.

Also a quick note on using subject as string: performance benefit seems negligible to me (e.g. about 1-2% overall allocations) at some point we should revisit though.

rng.GetBytes(randomBytes);
}

var newPrefix = new char[PrefixLength];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious. Is there a technical reason why _prefix shouldn't be preallocated since it'd be on TLS? I assume it'd be over optimization since this isn't on hot path. Only a question really.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, there is no reason. I implemented NUID a long time ago using locks and never changed this.

@mtmk mtmk merged commit caada52 into nats-io:main Oct 10, 2023
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

NUID Port
2 participants