runtime: .NET 6: System.Random - seeded implementation of .NextInt64* methods is inconsistent with .Next* methods

Description

.NET 6, 5, 3.1, and probably most .NET Frameworks:

var r1 = new Random(123);	
r1.Next().Dump(); // 2114319875
r1.Next().Dump(); // 1949518561
r1.Next().Dump(); // 1596751841
r1.Next().Dump(); // 1742987178
var r1 = new Random(123);	
r1.Next(0).Dump(); // 0 (technically does not need to consume state, but state got iterated)
r1.Next().Dump();  // 1949518561 (2nd call matches)
r1.Next().Dump();  // 1596751841 (3rd call matches)
r1.Next().Dump();  // 1742987178 (4th call matches)
var r1 = new Random(123);	
r1.Next(1).Dump(); // 0 (technically does not need to consume state, but state got iterated)
r1.Next().Dump();  // 1949518561 (2nd call matches)
r1.Next().Dump();  // 1596751841 (3rd call matches)
r1.Next().Dump();  // 1742987178 (4th call matches)
var r1 = new Random(123);
r1.Next((1 << 30) + 1).Dump(); // 1057159938 (iterates state only once)
r1.Next().Dump();              // 1949518561 (2nd call matches)
r1.Next().Dump();              // 1596751841 (3rd call matches)
r1.Next().Dump();              // 1742987178 (4th call matches)

.NET 6:

var r2 = new Random(123);
r2.NextInt64().Dump(); // 5751341171501756545
r2.NextInt64().Dump(); // 1468073721509004893
r2.NextInt64().Dump(); // 4492647383573077195
r2.NextInt64().Dump(); // 7443599010373778036
var r2 = new Random(123);
r2.NextInt64(0).Dump(); // 0 (state not consumed)
r2.NextInt64().Dump();  // 5751341171501756545 (different - 1st state consumption)
r2.NextInt64().Dump();  // 1468073721509004893 (different - 2nd state consumption)
r2.NextInt64().Dump();  // 4492647383573077195 (different - 3rd state consumption)
var r2 = new Random(123);
r2.NextInt64(1).Dump(); // 0 (state not consumed)
r2.NextInt64().Dump();  // 5751341171501756545 (different - 1st state consumption)
r2.NextInt64().Dump();  // 1468073721509004893 (different - 2nd state consumption)
r2.NextInt64().Dump();  // 4492647383573077195 (different - 3rd state consumption)
var r2 = new Random(123);
r2.NextInt64((1 << 62) + 1).Dump(); // 341812549 (state is consumed twice) 
r2.NextInt64().Dump();              // 4492647383573077195 (matches 4th result above)
r2.NextInt64().Dump();              // 7443599010373778036
r2.NextInt64().Dump();              // 1003052373149355376

Conclusions

Legacy seeded System.Random consumes state in stable fixed consumptions per API-call, regardless of parameters passed into API (as long as they are valid).

New .NextInt64* methods deviate from this expected (but not documented) behavior, and consume state in variable number of consumptions per API-call, depending on the input values.

Legacy seeded System.Random has always had the (undocumented) design of state-rotations being a function of the API calls only, but not the specific parameters. New .NextInt64* methods should have the same behavior to ensure consistency and reproducibility.

P.S.

System.Random test-suite is missing these scenarios…

@stephentoub @jkotas

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 16 (9 by maintainers)

Most upvoted comments

New .NextInt64* methods should have the same behavior to ensure consistency and reproducibility.

I’m not seeing why it’s important that new overloads exhibit the same undocumented side effects. Can you clarify why you believe it matters?

System.Random test-suite is missing these scenarios

It’s not “missing” them… this isn’t guaranteed behavior. The guarantee made is that the same input seed with the same sequence of calls will produce the same values, and that holds with the new overloads (and does have tests).

Closing this as by-design. As indicated above doc contributions/improvements are welcome if you’d like to see this updated.

The API docs are also open source and contributions are welcome: https://github.com/dotnet/dotnet-api-docs/blob/main/xml/System/Random.xml

@tannergooding What the current documentation does not allude to is that (N+1)th call to .Next* or .NextInt64* can return a different result depending on what parameters were passed into N-th call. This is completely non-obvious.

Seems like this is really just undefined behavior then, not contradictory behavior. Potential courses of action:

  1. Define the undefined behavior one way or another
  2. Explicitly document that the behavior is undefined (instead of implicitly, by omitting documentation on this facet)
  3. Do nothing and keep not mentioning this in the documentation (thus keeping it “undefined” by omission)