-
Notifications
You must be signed in to change notification settings - Fork 1.6k
<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
Conversation
There was a problem hiding this 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
stl/inc/experimental/random
Outdated
} | ||
|
||
inline void reseed() { | ||
_Engine().seed(random_device{}()); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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().
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()); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this 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...
stl/inc/experimental/random
Outdated
inline namespace fundamentals_v3 { | ||
|
||
inline default_random_engine& _Engine() { | ||
thread_local default_random_engine _Eng{random_device{}()}; |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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 avoidrandom_device
as it's stateless and its default constructor no longer constructs astring
.default_random_engine
ismt19937
for MSVC's STL:
Line 4938 in 8f94319
using default_random_engine = mt19937; - We cannot replace the TS's use of
default_random_engine
with directly usingrandom_device
, as that breaks the functionality ofreseed(value)
.
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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.
I want to mention that the randint facilities also include |
@@ -0,0 +1,50 @@ | |||
// random experimental header | |||
|
There was a problem hiding this comment.
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.
stl/inc/experimental/random
Outdated
inline namespace fundamentals_v3 { | ||
|
||
inline default_random_engine& _Engine() { | ||
thread_local default_random_engine _Eng{random_device{}()}; |
There was a problem hiding this comment.
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 avoidrandom_device
as it's stateless and its default constructor no longer constructs astring
.default_random_engine
ismt19937
for MSVC's STL:
Line 4938 in 8f94319
using default_random_engine = mt19937; - We cannot replace the TS's use of
default_random_engine
with directly usingrandom_device
, as that breaks the functionality ofreseed(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()); |
There was a problem hiding this comment.
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.
There was a problem hiding this 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).
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). |
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. |
Regarding the size: If you want to use TLS at |
I don't understand fibers, so I defer to your judgement.
I am definitely very concerned about any potential loader lock interaction with
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? |
This reverts commit dc7c56f.
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.
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 I talked with the designers of this feature and while they strongly care about |
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 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:
All of this may be solvable, but this is much more work than initially expected. Personally, I'd like to see |
A bit disappointing but completely understandable. Thanks for taking the time to review it :) |
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.
_Ugly
as perhttps://eel.is/c++draft/lex.name#3.1 or there are no product code changes.
verified by an STL maintainer before automated testing is enabled on GitHub,
leave this unchecked for initial submission).
members, adding virtual functions, changing whether a type is an aggregate
or trivially copyable, etc.).
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.