Skip to content

<experimental/random> Implement randint #361

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

Closed
wants to merge 16 commits into from

Conversation

SuperWig
Copy link
Contributor

@SuperWig SuperWig commented Dec 9, 2019

Description

Implemented std::experimental::randint from library fundamentals v3.

It's very common for beginners to want random ints for implementing things like tic-tac-toe or a variety of other terminal based games when they're learning C++.
However rand() is discouraged (I believe STL said even beginners shouldn't be using it - or something to that effect on reddit before) and <random> is not beginner friendly so I believe this would be a nice addition.

Checklist

Be sure you've read README.md and understand the scope of this repo.

If you're unsure about a box, leave it unchecked. A maintainer will help you.

  • Identifiers in product code changes are properly _Ugly as per
    https://eel.is/c++draft/lex.name#3.1 or there are no product code changes.
  • The STL builds successfully and all tests have passed (must be manually
    verified by an STL maintainer before automated testing is enabled on GitHub,
    leave this unchecked for initial submission).
  • These changes introduce no known ABI breaks (adding members, renaming
    members, adding virtual functions, changing whether a type is an aggregate
    or trivially copyable, etc.).
  • These changes were written from scratch using only this repository,
    the C++ Working Draft (including any cited standards), other WG21 papers
    (excluding reference implementations outside of proposed standard wording),
    and LWG issues as reference material. If they were derived from a project
    that's already listed in NOTICE.txt, that's fine, but please mention it.
    If they were derived from any other project (including Boost and libc++,
    which are not yet listed in NOTICE.txt), you must mention it here,
    so we can determine whether the license is compatible and what else needs
    to be done.

@SuperWig SuperWig requested a review from a team as a code owner December 9, 2019 11:46
Copy link
Contributor

@miscco miscco left a comment

Choose a reason for hiding this comment

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

As a bystander this looks good. I had some comments though

}

inline void reseed() {
_Engine().seed(random_device{}());
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be default_seed

Copy link
Contributor Author

@SuperWig SuperWig Dec 9, 2019

Choose a reason for hiding this comment

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

Then that wouldn't satisfy "The first form sets g to an unpredictable state". Default seed is predictable.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm I took this from the actual random library. It might be that they differ but maybe a maintainer can shed some light here.

Looking at the specification you linked to it seems that you need to reset the uniform_int_Distribution. At least that is what the note says.

Copy link
Contributor Author

@SuperWig SuperWig Dec 9, 2019

Choose a reason for hiding this comment

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

They do differ, the standard says seed() is equivalent to E() and that "Creates an engine with the same initial state as all other default-constructed engines of type E"

https://eel.is/c++draft/rand.req.eng#tab:rand.req.eng

Copy link
Contributor

@miscco miscco Dec 9, 2019

Choose a reason for hiding this comment

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

Yeah, I concure with that. This leaves only the reset of the distribution

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe I'm missing something but Isn't that taken care of by the fact that a new instance of uniform_int_distribution is created whenever randint is called?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think you are safe in the reseed case. However, in the usual case the result might differ, depending on whether the implementation of the distribution is statefull.

Imagine the simple case of int. The random_engine gives 64 bits of entropy. You only need 32 of those. So rather than throwing them away you can reuse them for the next call.

However, you are lucky as uniform_int_distribution is stateless

Copy link
Member

Choose a reason for hiding this comment

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

You only need 32 of those. So rather than throwing them away you can reuse them for the next call.

uniform_int_distribution can't do that because different calls to operator() may be passed different engines; it would be improper if the result depended on any engine other than that passed to operator().

@SuperWig
Copy link
Contributor Author

SuperWig commented Dec 9, 2019

As a bystander this looks good. I had some comments though

Thanks for the review. I'll tackle your comments later today unless the PR gets rejected before then.

unsigned long, unsigned long long>,
"template argument does not meet the requirements for IntType.");
_STL_ASSERT(_Min <= _Max, "_Min > _Max");
return uniform_int_distribution<_IntType>{_Min, _Max}(_Engine());
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not correct. You are creating a new uniform_int_distribution with a given range and Call operator() on that temporary. However, the specification speaks from a thread_local instance of a uniform_int_distribution. Now I fail to see how that is possible if there are different calls to randint with different parameters. @BillyONeal might know, at least i remember him talking about rngs.

In any case the initialization of the random number engines/distributions is generally rather costly.So recreating one seems Bad.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In any case the initialization of the random number engines/distributions is generally rather costly.So recreating one seems Bad.

AFAIK distributions aren't costly only engines. And the engine being used is a static thread_local (static is implied by thread_local unless I'm mistaken) instance that's returned by reference.

Copy link
Member

Choose a reason for hiding this comment

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

However, the specification speaks from a thread_local instance of a uniform_int_distribution.

That's not observable because uniform_int_distribution is stateless.

Copy link
Member

Choose a reason for hiding this comment

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

Specifically, in our current implementation, uniform_int_distribution is stateless (with respect to the random bits; of course the limits are stored), but the Standard permits random bits to be retained.

Copy link
Member

Choose a reason for hiding this comment

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

but the Standard permits random bits to be retained

It does? Huh, TIL. http://eel.is/c++draft/rand.req.dist#tab:rand.req.dist says there are only requirements on operator() if the same URBG is used successively.

Copy link
Contributor

Choose a reason for hiding this comment

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

http://eel.is/c++draft/rand.req.dist#tab:rand.req.dist says there are only requirements on operator() if the same URBG is used successively.

That's imposing the uniformity requirement. If you invoke a distribution repeatedly with different engines, then uniformity isn't guaranteed. Indeed it could not be, since I could invoke the distribution repeatedly with the same engine state.

The hint that distributions are allowed to cache some random bits is the specification (and existence) of reset, which guarantees that "Subsequent uses of d do not depend on values produced by any engine prior to invoking reset." (i.e., any cached bits are flushed).

Copy link
Member

@BillyONeal BillyONeal left a comment

Choose a reason for hiding this comment

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

The thread_local issue might be a spec bug but there's a reason we've avoided implementing stuff not in the WP...

inline namespace fundamentals_v3 {

inline default_random_engine& _Engine() {
thread_local default_random_engine _Eng{random_device{}()};
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we can use thread_local in standard headers. I know the spec talks about this, but IMO this is one of the things we need to fix in the spec before it would be ready to actually merge.
I think we should return rand_s or _Random_device() (which calls rand_s) instead of replacing the platform's good and secure RNG with a poor linear congruential engine...

Copy link
Member

Choose a reason for hiding this comment

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

  • What's wrong with thread_local, if it's mandated by the TS?
  • rand_s() is not available within STL headers (as the CRT guards its declaration with a macro). _Random_device() is available, but I see no reason to avoid random_device as it's stateless and its default constructor no longer constructs a string.
  • default_random_engine is mt19937 for MSVC's STL:
    using default_random_engine = mt19937;
  • We cannot replace the TS's use of default_random_engine with directly using random_device, as that breaks the functionality of reseed(value).

Copy link
Member

@BillyONeal BillyONeal Dec 10, 2019

Choose a reason for hiding this comment

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

What's wrong with thread_local, if it's mandated by the TS?

The TS doesn't know that that causes a .TLS section to be emitted and that there are a limited number of those. If any .obj in kernel32.dll included the header, for example, that would critically break any program near the ~100 or so TLS slot limit.

We put other facilities that require thread-local like behavior in the vcruntime PTD structure, in part to avoid such mess. (And also to make fibers work)

mersenne_twister/default_random_engine are particularly problematic because they are huge, so a program that creates many threads which was just under hardware/implementation limits before might be over those limits after. In that sense, thread_local breaks the "don't pay for what you don't use" principle because most threads aren't going to use all thread_local data members.

rand_s() is not available within STL headers

Right, I'm referring to the facility (that being Windows default cryptographically secure random engine)

We cannot replace the TS's use of default_random_engine with directly using random_device, as that breaks the functionality of reseed(value).

Yeah, I wasn't sure if the reseed function was an artifact of the recommended implementation or a hard requirement driven by design goals. I asked on L[E]WG mailing lists and it was indicated there that the ability to reseed is indeed a design goal.

We should decide whether a created thread is supposed to derive its random state from the calling thread (like locale does) or if it should get a brand new state.

unsigned long, unsigned long long>,
"template argument does not meet the requirements for IntType.");
_STL_ASSERT(_Min <= _Max, "_Min > _Max");
return uniform_int_distribution<_IntType>{_Min, _Max}(_Engine());
Copy link
Member

Choose a reason for hiding this comment

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

However, the specification speaks from a thread_local instance of a uniform_int_distribution.

That's not observable because uniform_int_distribution is stateless.

@zhihaoy
Copy link

zhihaoy commented Dec 9, 2019

I want to mention that the randint facilities also include std::experimental::sample and std::experimental::shuffle.

@@ -0,0 +1,50 @@
// random experimental header

Copy link
Member

Choose a reason for hiding this comment

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

I am willing to make a special exception to our non-goal of "Implementing Technical Specifications" on a case-by-case basis for this feature, given how obnoxious the lack of easy random number generation is for beginners, but I am concerned about permanently increasing our support cost, and the lack of progress for this proposal within WG21.

inline namespace fundamentals_v3 {

inline default_random_engine& _Engine() {
thread_local default_random_engine _Eng{random_device{}()};
Copy link
Member

Choose a reason for hiding this comment

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

  • What's wrong with thread_local, if it's mandated by the TS?
  • rand_s() is not available within STL headers (as the CRT guards its declaration with a macro). _Random_device() is available, but I see no reason to avoid random_device as it's stateless and its default constructor no longer constructs a string.
  • default_random_engine is mt19937 for MSVC's STL:
    using default_random_engine = mt19937;
  • We cannot replace the TS's use of default_random_engine with directly using random_device, as that breaks the functionality of reseed(value).

unsigned long, unsigned long long>,
"template argument does not meet the requirements for IntType.");
_STL_ASSERT(_Min <= _Max, "_Min > _Max");
return uniform_int_distribution<_IntType>{_Min, _Max}(_Engine());
Copy link
Member

Choose a reason for hiding this comment

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

Specifically, in our current implementation, uniform_int_distribution is stateless (with respect to the random bits; of course the limits are stored), but the Standard permits random bits to be retained.

Copy link
Member

@StephanTLavavej StephanTLavavej left a comment

Choose a reason for hiding this comment

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

After refactoring reseed() for use in engine initialization, I believe that this will be ready to merge (with test changes added in the Microsoft-internal repo; we are getting ever closer to making the tests public).

@StephanTLavavej StephanTLavavej self-assigned this Dec 12, 2019
@BillyONeal
Copy link
Member

After refactoring reseed() for use in engine initialization, I believe that this will be ready to merge (with test changes added in the Microsoft-internal repo; we are getting ever closer to making the tests public).

I think we still need the fiber resistance added that the rest of the "thread_local" ish state from the libraries provide. Ideally we would staple this to the vcruntime ptd somehow, but given how large a mersenne twister is and the relative unlikelihood of this actually making it into the standard maybe it's too early to commit to paying that cost on every thread and fiber forever.

As a result I remain weakly opposed to merging this feature unless we have some means of preventing users who don't use the experimental feature from paying costs related to the experimental feature (thread_local makes everyone pay that cost, whether they touch this feature or not).

@BillyONeal
Copy link
Member

I just realized that this might also be triggering the DllMain / loader lock monster. Won't this try to call RtlGenRandom (via rand_s, via _Random_device, via random_device::operator()) under the loader lock? That's not safe.

@MikeGitb
Copy link

MikeGitb commented Dec 12, 2019

Regarding the size: If you want to use TLS at ADL all, I think this would be a perfectly valid case for allocating the engine on the heap and only storing a ptr to it on the TLS.

@StephanTLavavej
Copy link
Member

[@BillyONeal]

I think we still need the fiber resistance added that the rest of the "thread_local" ish state from the libraries provide.

I don't understand fibers, so I defer to your judgement.

some means of preventing users who don't use the experimental feature from paying costs related to the experimental feature (thread_local makes everyone pay that cost, whether they touch this feature or not).

<experimental/algorithm> and <experimental/random> are header-only and leaf headers, so it's weakly "you don't pay for what you don't include". But that's a valid reason to be opposed.

I just realized that this might also be triggering the DllMain / loader lock monster. Won't this try to call RtlGenRandom (via rand_s, via _Random_device, via random_device::operator()) under the loader lock? That's not safe.

I am definitely very concerned about any potential loader lock interaction with thread_local initialization. We should probably ask a compiler dev if we want to merge this implementation.

[@MikeGitb]

Regarding the size: If you want to use TLS at ADL, I think this would be a perfectly valid case for allocating the engine on the heap and only storing a ptr to it on the TLS.

That's slightly observable (new could be replaced with an instrumented version), but that is a potential workaround.

@MikeGitb
Copy link

That's slightly observable (new could be replaced with an instrumented version), but that is a potential workaround.

Is the stdlib not allowed to create hidden calls to new?

@BillyONeal
Copy link
Member

That's slightly observable (new could be replaced with an instrumented version), but that is a potential workaround.

Is the stdlib not allowed to create hidden calls to new?

We're allowed to but try to not unless the standard explicitly tells us to do so.

Regarding the fibers stuff, that would mean attaching it to an FLS slot just like we do elsewhere; dig around in your Visual Studio sources and you should see e.g. __acrt_get_ptd, __vcrt_get_ptd etc. "ptd" stands for "per thread data" but it's actually per fiber data in modern versions of the CRT. The "acrt" ones are part of ucrtbase.dll (part of Windows). e.g. on this system I'm talking about these:

  • C:\Program Files (x86)\Microsoft Visual Studio\2019\Preview\VC\Tools\MSVC\14.24.28314\crt\src\vcruntime\dll_dllmain.cpp
  • C:\Program Files (x86)\Microsoft Visual Studio\2019\Preview\VC\Tools\MSVC\14.24.28314\crt\src\vcruntime\per_thread_data.cpp

To do this randint feature really correctly we would probably want to copy exactly what that ptd stuff is doing there, and put it in a new satellite DLL so that the state of reseed is shared correctly across DLL boundaries just like other shared standard library state (e.g. default_memory_resource).

I talked with the designers of this feature and while they strongly care about reseed, they do not strongly care about the actual engine used being default_random_engine. They filed a new defect report: LWG-3357. As a result we might want to ask @imneme and contributors if they'd be willing to add the LLVM exception and we'd use PCG, which has better distribution characteristics than mersenne twister and also doesn't need a huge state.

@StephanTLavavej
Copy link
Member

Thanks again for working on this and going through our lengthy review process. Now that we're back from the holidays and catching up on our PR backlog, we've discussed what to do about randint.

We've decided against merging this PR, due to our concerns with how the feature as currently specified in the TS interacts with our platform. There's too much risk (and support cost) to justify making an exception to our usual Non-Goals policy regarding implementing TS features.

As mentioned above, we're concerned about:

  • Interactions with our per thread/fiber data and DLLs
  • Ensuring that loader lock problems don't happen while initializing the engine's state
  • The size of Mersenne Twister potentially causing issues

All of this may be solvable, but this is much more work than initially expected.

Personally, I'd like to see randint refined for eventual Standardization, and I would also like to see PCG's high-quality generators proposed for Standardization (ideally with a reference implementation that's Apache+LLVM licensed so we can directly use it), which would resolve Mersenne Twister size concerns. Then I think we could figure out the PTD and initialization issues.

@SuperWig
Copy link
Contributor Author

A bit disappointing but completely understandable. Thanks for taking the time to review it :)

@SuperWig SuperWig deleted the exp_random branch January 25, 2020 10:10
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.

7 participants