Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.

Added GetInt32 to RandomNumberGenerator. #31243

Merged
merged 1 commit into from Jul 25, 2018
Merged

Added GetInt32 to RandomNumberGenerator. #31243

merged 1 commit into from Jul 25, 2018

Conversation

vcsjones
Copy link
Member

@vcsjones vcsjones commented Jul 21, 2018

Implement RNG.GetInt32.

/cc @bartonjs @GrabYourPitchforks

Fixes #30873.


public static int GetInt32(int toExclusive)
{
if (toExclusive <= 0) throw new ArgumentOutOfRangeException(nameof(toExclusive), SR.ArgumentOutOfRange_NeedPosNum);
Copy link
Member

Choose a reason for hiding this comment

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

throw on next line. The next function happens to violate our style (rule 1, at that). If you wanted to clean it up I wouldn't mind.

Span<uint> valueResult = stackalloc uint[1];
ref uint result = ref valueResult[0];
Span<byte> valueResultBytes = MemoryMarshal.AsBytes(valueResult);
if (BitConverter.IsLittleEndian)
Copy link
Member

Choose a reason for hiding this comment

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

My personal style is to have a blank line before a control flow statement because I find it helps legibility. I'm just offering it out there, our style guide isn't prescriptive on this one way or another.

public static void GetInt32_FullRange()
{
int result = RandomNumberGenerator.GetInt32(int.MinValue, int.MaxValue);
Assert.NotEqual(0, result);
Copy link
Member

Choose a reason for hiding this comment

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

There's a 1 in 4 billion chance this fails. If you want to assert something you should just assert it wasn't int.MaxValue

observedNumbers[number]++;
}
}
const double tollerance = 0.07;
Copy link
Member

Choose a reason for hiding this comment

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

typo: tolerance

foreach ((_, int occurences) in observedNumbers)
{
double percentage = occurences / (double)numbers.Length;
Assert.True(Math.Abs(expected - percentage) < tollerance, "Occured number of times within threshold.");
Copy link
Member

Choose a reason for hiding this comment

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

Typo: Occurred

double percentage = occurences / (double)numbers.Length;
Assert.True(Math.Abs(expected - percentage) < tollerance, "Occured number of times within threshold.");
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

It would be good to see range validity test for negative bounds, and some tests with interesting-looking bounds sizes (really I want to test both the lzcnt implementation and the algorithm. Something annoying like (x, x + 0x0101) so the high bit in the mask value is technically required, but rarely valid. If you ran that 1000 times you "should" have at least one hit on the highest value, and shouldn't have value skew.)

(int.MinValue, int.MinValue + 3)
(-257, -129)
(-100, 5)
(254, 512)
...

Also, roll 1000d6, check for skew. (The existing "coinflip" test sort of does this, but it's bit-aligned in range, so it doesn't test the high value rejection vs mod bias algorithm differential)

@vcsjones
Copy link
Member Author

vcsjones commented Jul 22, 2018

I'm not fully sure what the failure in d243963 was. It looks like infrastructure? The most information I could glean was a workItemStatus had a failure count of 1, but the test failure count was 0.

Update: "mission control" says it was System.IO.Packaging.Tests.

@vcsjones
Copy link
Member Author

@bartonjs @jkotas thank you for the (quick!) code review. I think I have addressed all feedback.


// We only want to generate as many bytes as required to satisfy the mask to not apply
// undue pressure to the underlying random number generator.
int bytesRequired = (32 + 7 - leadingZeroCount) / 8;
Copy link
Member

Choose a reason for hiding this comment

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

Maybe comment where the 7 comes from?

@vcsjones
Copy link
Member Author

Test failures look unrelated.

@vcsjones
Copy link
Member Author

@GrabYourPitchforks @bartonjs OK. Changed, rebased, squashed.

@GrabYourPitchforks GrabYourPitchforks merged commit 928873f into dotnet:master Jul 25, 2018
@vcsjones vcsjones deleted the 30873-rng-integers branch July 25, 2018 03:28
@karelz karelz added this to the 3.0 milestone Aug 21, 2018
@khellang
Copy link
Member

@terrajobst Candidate for api-candidate-for-standard Marks APIs that are tracked to be included in the next version of the standard. ? 🤔

@puenktchen
Copy link

@vcsjones Sorry for misusing this platform for contacting you. For several reasons i'm stuck at Netstandard 2.0 for my project, so i wanted to add your GetInt32 method as an extension to RandomNumberGenerator. Unfortunately i have a problem with "RandomNumberGeneratorImplementation.FillSpan()." Where does it come from? What does it do? Can you shed some light on it please?

@vcsjones
Copy link
Member Author

vcsjones commented Oct 10, 2022

@puenktchen It's the internal implementation for RandomNumberGenerator in .NET Core that can fill a Span<byte>. .NET Standard 2.0 has no such API, so you'll have to allocate and fill a byte array.

This will mean the GetInt32 will allocate 4 bytes per-invocation, but to keep the implementation straight forward it's probably the best thing to do.

Disclaimer: The snippet below is untested for correctness. The only testing on the code below that I have done is that it compiles for netstandard2.0.

static class Extensions
{
    public static int GetInt32(this RandomNumberGenerator rng, int toExclusive)
    {
        return GetInt32(rng, 0, toExclusive);
    }

    public static int GetInt32(this RandomNumberGenerator rng, int fromInclusive, int toExclusive)
    {
        if (fromInclusive >= toExclusive)
            throw new ArgumentException("Range is invalid");

        uint range = (uint)toExclusive - (uint)fromInclusive - 1;

        // If there is only one possible choice, nothing random will actually happen, so return
        // the only possibility.
        if (range == 0)
        {
            return fromInclusive;
        }

        // Create a mask for the bits that we care about for the range. The other bits will be
        // masked away.
        uint mask = range;
        mask |= mask >> 1;
        mask |= mask >> 2;
        mask |= mask >> 4;
        mask |= mask >> 8;
        mask |= mask >> 16;

        byte[] resultBuffer = new byte[sizeof(uint)];
        uint result;

        do
        {
            rng.GetBytes(resultBuffer);
            result = mask & BitConverter.ToUInt32(resultBuffer, 0);
        }
        while (result > range);

        return (int)result + fromInclusive;
    }
}

@puenktchen
Copy link

@vcsjones Works perfect for me. Thank you so much!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
8 participants