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

Optimize Random{NumberGenerator}.GetItems for power-of-two choices #92229

Merged
merged 6 commits into from
Sep 19, 2023

Conversation

stephentoub
Copy link
Member

When possible, make a single call to get the required randomness rather than one per element.

Closes #92130

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Security.Cryptography;

BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);

[HideColumns("Error", "StdDev", "Median", "RatioSD")]
public class Tests
{
    private const int Length = 11;
    private static readonly string s_charChoices = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
    private static ReadOnlySpan<int> s_intChoices => [42, 43, 44, 45, 46, 47, 48, 49];
    private static int[] s_dest = new int[Length];

    [Benchmark]
    public string RngGetString() => RandomNumberGenerator.GetString(s_charChoices, Length);

    [Benchmark]
    public void RandGetItems() => Random.Shared.GetItems(s_intChoices, s_dest);
}
Method Toolchain Mean Ratio
RngGetString \main\corerun.exe 764.18 ns 1.00
RngGetString \pr\corerun.exe 98.25 ns 0.13
RandGetItems \main\corerun.exe 57.21 ns 1.00
RandGetItems \pr\corerun.exe 16.56 ns 0.29

When possible, make a single call to get the required randomness rather than one per element.
@ghost
Copy link

ghost commented Sep 18, 2023

Tagging subscribers to this area: @dotnet/area-system-security, @bartonjs, @vcsjones
See info in area-owners.md if you want to be subscribed.

Issue Details

When possible, make a single call to get the required randomness rather than one per element.

Closes #92130

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Security.Cryptography;

BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);

[HideColumns("Error", "StdDev", "Median", "RatioSD")]
public class Tests
{
    private const int Length = 11;
    private static readonly string s_charChoices = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
    private static ReadOnlySpan<int> s_intChoices => [42, 43, 44, 45, 46, 47, 48, 49];
    private static int[] s_dest = new int[Length];

    [Benchmark]
    public string RngGetString() => RandomNumberGenerator.GetString(s_charChoices, Length);

    [Benchmark]
    public void RandGetItems() => Random.Shared.GetItems(s_intChoices, s_dest);
}
Method Toolchain Mean Ratio
RngGetString \main\corerun.exe 764.18 ns 1.00
RngGetString \pr\corerun.exe 98.25 ns 0.13
RandGetItems \main\corerun.exe 57.21 ns 1.00
RandGetItems \pr\corerun.exe 16.56 ns 0.29
Author: stephentoub
Assignees: -
Labels:

area-System.Security

Milestone: -

@adamsitnik adamsitnik added this to the 9.0.0 milestone Sep 18, 2023
@adamsitnik adamsitnik added the tenet-performance Performance related issue label Sep 18, 2023
Use separate scratch buffer instead of destination buffer. Upside, we don't need to be concerned about temporarily corrupt Ts, and it can work for managed Ts. Downside, it requires separate scratch space, which means an ArrayPool rental for larger sizes.
Loop over a stackalloc'd buffer rather than using ArrayPool
@Ponant
Copy link
Contributor

Ponant commented Sep 18, 2023

A few remarks if you do not mind.

  1. I tested @bartonjs method against mine and confirm it is faster, but it is mainly due to the use bitwise AND instead of integer division, and not the unsafe territory. At least on my machine.

  2. I think the benefit of the pow2 non-negligible speedup should be documented in the methods that use it such as GetString, perhaps...

  3. I have an addional suggestion, which I think can be beneficial to users and which can be implemented in the same PR. But I do not know if oyu prefer that I create another issue or drop it here? it is about rewritting GetBytes to allow users to have a byte array where each byte is bound to [0,N] where N is a power of two. For N=256, we recover the case of the current GetBytes. Let me know I will drop a code

@stephentoub
Copy link
Member Author

I have an addional suggestion, which I think can be beneficial to users and which can be implemented in the same PR. But I do not know if oyu prefer that I create another issue or drop it here? it is about rewritting GetBytes to allow users to have a byte arrays where each byte is bound to [0,N] where N is a power of two. For N=256, we recover the case of the current getBytes. Let me know I will drop a code

This sounds very niche, and the savings is effectively an array access per element. I don't think it's worth special-casing without seeing more examples of where it would be really meaningful.

I think the benefit of the pow2 non-negligible speedup should be documented in the methods that use it such as GetString, perhaps...

It's an implementation detail. We avoid documenting such optimization implementations, as we reserve the right to change them at any point.

@Ponant
Copy link
Contributor

Ponant commented Sep 18, 2023

I have an addional suggestion, which I think can be beneficial to users and which can be implemented in the same PR. But I do not know if oyu prefer that I create another issue or drop it here? it is about rewritting GetBytes to allow users to have a byte arrays where each byte is bound to [0,N] where N is a power of two. For N=256, we recover the case of the current getBytes. Let me know I will drop a code

This sounds very niche, and the savings is effectively an array access per element. I don't think it's worth special-casing without seeing more examples of where it would be really meaningful.

A) Generate random booleans
B) Generate Random vectors in 4 dimensions (my case)

@stephentoub
Copy link
Member Author

I have an addional suggestion, which I think can be beneficial to users and which can be implemented in the same PR. But I do not know if oyu prefer that I create another issue or drop it here? it is about rewritting GetBytes to allow users to have a byte arrays where each byte is bound to [0,N] where N is a power of two. For N=256, we recover the case of the current getBytes. Let me know I will drop a code

This sounds very niche, and the savings is effectively an array access per element. I don't think it's worth special-casing without seeing more examples of where it would be really meaningful.

A) Generate random booleans B) Generate Random vectors in 4 dimensions (my case)

I don't follow. Are you proposing a new API? Or are you proposing that the implementation special-case typeof(T) == typeof(bool) and validates that choices[0] == false and choices[1] == Unsafe.As<byte, bool>(1)? And for typeof(T) == typeof(byte), looping through all of the choices as a precompute step to make sure that choices[i] == (byte)i?

@GrabYourPitchforks
Copy link
Member

@stephentoub Consider also duplicating the test here with one that passes a power-of-two sized values span. That way we get distribution tests across both implementations which now exist in this method.

public static void GetString_RandomDistribution()
{
const string Choices = "abcdefghijklmnopqrstuvwxyz";
string generated = RandomNumberGenerator.GetString(Choices, 10_000);
VerifyDistribution<char>(generated, 1f / 26);
VerifyAllInRange(generated, 'a', (char)('z' + 1));
}

@Ponant
Copy link
Contributor

Ponant commented Sep 18, 2023

The current GetBytes returns an array of bytes, where each byte can take any value from 0 to 255. My proposal is to have GetBytes to also allow me to return an array of bytes but where each byte is bound to [0,N] where N is a power of two. For example, I can generate a byte array of bools 011100011010101 or random quaternions or any 4d vector, or octonions or above powers.

I am using something like that, it is essentially the same spirit as you do here with the new GetString in this PR, but for getting byte arrays. I am using something like that (checks on parameters omitted for brievety)

private static byte[] GetRandomBytes(int length, int maxNumOfBytes)
 {
        Span<byte> bytes = length <= _stackAllocThreshold ? stackalloc byte[length] : new byte[length];

        // Generate random bytes, each taking values between 0 and 255
        RandomNumberGenerator.Fill(bytes);

        // We do not need to loop if we cover all byte possibilities, since  RandomNumberGenerator.Fill does this 
        if (maxNumOfBytes != 256)
        {
            byte mask = (byte)(maxNumOfBytes - 1);

            // make each byte between 0 and maxNumOfBytes
            for (var i = 0; i < bytes.Length; i++)
                bytes[i] &= mask;

        }

        return bytes.ToArray();
}

@stephentoub
Copy link
Member Author

The current GetBytes returns an array of bytes, where each byte can take any value from 0 to 255. My proposal is to have GetBytes to also allow me to return an array of bytes but where each byte is bound to [0,N] where N is a power of two.

That's a new API. Feel free to open a separate proposal for one. That won't be done as part of an optimization PR.

@vcsjones
Copy link
Member

The current GetBytes returns an array of bytes, where each byte can take any value from 0 to 255. My proposal is to have GetBytes to also allow me to return an array of bytes but where each byte is bound to [0,N] where N is a power of two. For example, I can generate a byte array of bools 011100011010101 or random quaternions or any 4d vector, or octonions or above powers.

This would be a new API proposal.

We can discuss the merits and usefulness of the proposal there to keep this PR focused on the perf of existing APIs.

@stephentoub stephentoub merged commit 4d948a0 into dotnet:main Sep 19, 2023
@stephentoub stephentoub deleted the randpow2 branch September 19, 2023 00:42
@ghost ghost locked as resolved and limited conversation to collaborators Oct 19, 2023
@bartonjs bartonjs added tracking This issue is tracking the completion of other related issues. cryptographic-docs-impact Issues impacting cryptographic docs. Cleared and reused after documentation is updated each release. labels Aug 15, 2024
@stephentoub stephentoub added breaking-change Issue or PR that represents a breaking API or functional change over a prerelease. needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet labels Sep 19, 2024
@stephentoub
Copy link
Member Author

@bartonjs, per your feedback on my recent PR, I've marked this as a breaking change to be documented.

@stephentoub
Copy link
Member Author

Opened #108018 to revert the breaking change aspect.

@stephentoub stephentoub removed breaking-change Issue or PR that represents a breaking API or functional change over a prerelease. needs-breaking-change-doc-created Breaking changes need an issue opened with https://github.com/dotnet/docs/issues/new?template=dotnet labels Sep 19, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Security cryptographic-docs-impact Issues impacting cryptographic docs. Cleared and reused after documentation is updated each release. tenet-performance Performance related issue tracking This issue is tracking the completion of other related issues.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

RandomNumberGenerator bad performance on GetString and possible reason
7 participants