C vs C++, from the author of ZeroMQ
-
Yup.
Of course you can tell syslogd not to bother logging any messages that look like the ones from the Ethernet driver, which I did as soon as I found out what was going on, but there's so much overhead involved in doing that as to make it pointless running the wire at gigabit speeds in the first place.
The Beaglebone Black has only a 100Mb/s Ethernet port, not a gigabit one, but that's really all I need for an Internet router anyway; the cubietruck is back to a no-VLANs Ethernet configuration and happily serving files to the LAN at relatively decent speeds.
-
This was neither. This was me fucking up, and getting shitty feedback from java.
JVM counts as compiler to me.
-
There are some things nothing can protect us from. When I say "always", I mean "except things I can't do anything about". Compiler bugs, executable code corruption and cosmic rays randomly shifting bits in RAM just happen.
But you can do things about it. None of my examples are "bad code", corruption or the CPU misbehaving because of cosmic rays. I simply gave examples of someone using legal constructs of the language to break your expectations of their behavior. Things that are sometimes necessary because the real world is messy and languages that try to be perfect don't cut it in the real world.
C++ is not widespread despite it's warts. It's widespread because of them. Even Rust, that tries to clean those warts, had to compromise with the
unsafe
keyword. You can't ignore that just because it doesn't suit you. It's part of the language, and it will be used.Your stance seems to be that unless you know everything, there is chaos and attempts to handle that are futile. My stance is that chaos is a given, and that you code to account for it.
-
But you can do things about it.
Only by being unreasonably paranoid.None of my examples are "bad code", corruption or the CPU misbehaving because of cosmic rays.
I consider stupidity the same kind of inexorable disaster.C++ is not widespread despite it's warts. It's widespread because of them.
Um, no, not at all. It's widespread because of source code compatibility with C. Just like PHP is widespread for no technical reason, but for availability of cheap web hostings in 2000s.Even Rust, that tries to clean those warts, had to compromise with the unsafe keyword. You can't ignore that just because it doesn't suit you. It's part of the language, and it will be used.
In a well-formed Rust program, allunsafe
blocks, while they can violate some guarantees, they should restore all those guarantees at the end of block. Doing anything else makes the program ill-formed - so doing this is sheer stupidity, which I consider an inexorable disaster you can't do anything about without invoking paranoia.Your stance seems to be that unless you know everything, there is chaos and attempts to handle that are futile.
Not at all. In a typical C++ code, you can expect some things to work some certain way. You can, for example, expectoperator+
to not mutate the operands. Everyone is subconsciously heavily depending on hundreds of these little assumptions, and everything usually goes well, because most code is mostly sane. But it's all because we believe in it. We believe GCC generates correct code. We believe boost::filesystem sees all files. We believe assignment operator makes a logical duplicate of rhs. We have no guarantee of any of it. We especially don't have a guarantee that exceptions won't be thrown by a particular piece of code, for example a constructor (it's changed with C++11 somewhat). I just took the fact that those beliefs are sometimes wrong, and taken to logical extreme (logical, as in such level of shattering dreams that still allows things to work).Rust explicitly gives some of these guarantees, so we can not only believe in them, but actually have a reason to believe. Rust raises our level of comfort from "I think this should work" to "this will work". It's a giant relief for all developers - both in writing new code (constant sanity checks made by compiler) and in using libraries (because the devs of this library went through the same process). Of course, it's all made possible because we believe people don't abuse
unsafe
. Which is as reasonable as expecting C++ programmers to enable warnings.
-
Only by being unreasonably paranoid.
If paranoia is the only way to bridge the gap between "possible" and "impossible", I wouldn't call it unreasonable.
Um, no, not at all. It's widespread because of source code compatibility with C.
You don't think compatibility with C is a wart? You think Stroustrup wanted to design a language that opposes most of the conventions of C, but is required to compile C code with minimal modifications? C compatibility is a compromise in the language design. A wart. Given C++'s longevity, it was probably the right compromise at the time.C++ is not a better language for being compatible with C, despite all the effort that implied, but it is a more widespread one.
We especially don't have a guarantee that exceptions won't be thrown by a particular piece of code, for example a constructor (it's changed with C++11 somewhat).
Um, what? Throwing an exception is the way constructors are meant to signal errors. It's why when you don't use exceptions, you have to create the object and then initialize it.Destructors shouldn't let exceptions escape them (it's fine if the destructor calls a function that throws, and catches that exception), so if you meant that and typo'd it, ignore the previous paragraph.
Still, while being central to the design of C++, it is true that the proper way of using them isn't fixed. Exception specifications are one of the things that have changed the most, and are still somewhat in flux, though it seems they're finally finding a reasonable way of specifying them. Which mostly amounts to making a promise that a piece of code won't let an exception escape it, and calling terminate if it violates that promise.
But ok, you're saying that misusing "unsafe" amounts to undefined behavior. I agree that trying to fight undefined behavior is pointless. That still leaves the problem of having the entire behavior documented. It might be possible in Rust, but you are eventually going to call something that is not Rust, by linking to a dll for example, and other languages don't offer the same guarantee of safety that Rust does. A C function could return anything it wants as an error, and that would be well specified behavior for that language.
And your Rust program is sitting on top of an operating system written in C or C++, most likely. So until you design a Rust operating system, you can't really say that you know what every error returned from every call you make could be.
-
You think Stroustrup wanted to design a language that opposes most of the conventions of C, but is required to compile C code with minimal modifications?
That's EXACTLY how it was. Why do you think the Most Vexing Parse is a thing?C++ is not a better language for being compatible with C, despite all the effort that implied, but it is a more widespread one.
Agreed. It's not better, it's worse. But you seem to underestimate how big role C compatibility played in popularizing C++.Um, what? Throwing an exception is the way constructors are meant to signal errors. It's why when you don't use exceptions, you have to create the object and then initialize it.
What are the possible errorsclass Vec2f { float x, y; }
can encounter in constructor? If you're a reasonable person expecting reasonable behavior, you'd say none. But in C++98/03, anything can throw anything, so you must prepare for this constructor to throw as well.Exceptions in constructors are especially nasty because they leave behind a half-constructed object in indeterminate state.
It might be possible in Rust, but you are eventually going to call something that is not Rust, by linking to a dll for example, and other languages don't offer the same guarantee of safety that Rust does.
FFI in Rust requiresunsafe
, for exactly this reason ;)So until you design a Rust operating system
It's on the way already
-
That's EXACTLY how it was.
But you seem to underestimate how big role C compatibility played in popularizing C++.
Umm, I don't think you are understanding me. Stroustrup wanted a language that gave the user the ability to create their own abstractions, types that behaved essentially the way built in types could, but that also provided zero overhead for those abstractions, and access to the hardware when necessary.
Aside from what he expected of the language itself, he wanted his language to be used by people. He didn't choose to bolt things on to C because that was the easiest way to achieve his goals on the language front. He did it because it was the best way to achieve widespread use in the demographic he was targeting the language at.
So I'm not underestimating how big a role C compatibility had. I'm doing the opposite, I'm saying C compatibility is only there so that C++ would be popular. The only benefit C compatibility offered was popularity, but it was a valuable enough benefit to sacrifice the syntax of the language to it.
Exceptions in constructors are especially nasty because they leave behind a half-constructed object in indeterminate state.
Remember when I said I had to correct you about basic concepts, and how that kind of lowered the merit of your arguments? This is another BASIC concept regarding exceptions that you misunderstand in a critical manner.The result of throwing in a constructor is that the object doesn't exist. It is impossible to refer to an object that threw in the constructor. The syntax itself doesn't allow it. Simple example:
class MyClass; void Function1() { MyClass anObject; DoSomething(anObject); }
If the constructor of
anObject
throws, anObject is never constructed, andDoSomething
is never called. Instead, the caller of Function1 simply sees the exception, and the exception will continue to propagate until a handler catches it. Another example:void Function2() { try { MyClass anObject; } catch(MyException& e) {} // DoSomething(anObject); // This is a syntax error. // anObject doesn't exist in this scope }
Constructor exceptions are the way you avoid having zombie objects. With exceptions, you either have a valid object, or an exception. You can't have zombie objects. Trying to refer to an object that wasn't constructed is a syntax error.
-
He didn't choose to bolt things on to C because that was the easiest way to achieve his goals on the language front. He did it because it was the best way to achieve widespread use in the demographic he was targeting the language at.
I know it all. But it doesn't change the fact that he made a goal of being able to compile C as C++, he achieved it, and the language ended up much worse than it could because of it.The only benefit C compatibility offered was popularity, but it was a valuable enough benefit to sacrifice the syntax of the language to it.
I think it wasn't worth it in the long run, after all.It is impossible to refer to an object that threw in the constructor.
Whenever you have the throwing object in the same scope as the try block, you can. For example, if it's a class member, or if using placement new. However, when I was trying to reproduce it in Ideone, I kept getting SIGABRT whenever I managed to get half-initialized object. I blame using a hard-coded case and compiler optimizing it out. Too bored for more testing, though.
-
Can you show an example of what you mean? The syntax you used?
-
I know it all. But it doesn't change the fact that he made a goal of being able to compile C as C++, he achieved it, and the language ended up much worse than it could because of it.
Which is the point I made in the first place. C++ is popular not in spite of its warts, it's popular because of them. Glad that you now agree.
-
Can you show an example of what you mean? The syntax you used?
#include <iostream> using namespace std; class Motherfucker { std::string s; public: Motherfucker() { s = "Fuck the system"; throw "They try to catch me"; } void iDoWhatIWant() { cout << s << endl; } }; class Russkiy { Motherfucker mofo; public: Russkiy() try: mofo() {} catch(...) {} void shtoTyDielosh() { mofo.iDoWhatIWant(); } }; int main() { Russkiy wania; wania.shtoTyDielosh(); }
The placement new example would be rather straight forward.
Which is the point I made in the first place. C++ is popular not in spite of its warts, it's popular because of them. Glad that you now agree.
Well, at first, I thought you meant all the C++ warts, not just the C legacy-related ones. Because C++ has many warts unrelated to C - for example, stateful globals for printing to standard streams, overloading bitshifts to mean something entirely else, implicit constructors shitfest, const_cast, auto_ptr, generator-unfriendly range-for in C++11, etc. These certainly don't add to its popularity.
-
Your code crashes because you're not catching the exception you're throwing.
@C++ Standard §15.3p15 said:The currently handled exception is rethrown if control reaches the end of a handler of the function-try-block of a constructor or destructor.
-
Oh, I see. So the only way to get half-initialized object is either placement new, or a buggy custom allocator.
Score for exceptions.
But on further thought, there's still the problem of destructor handling constructor's exception.
-
I didn't think anyone else was still reading this thread. And yet, I got 'd.
Yeah, you can't catch exceptions in a constructor. Or rather, you can catch them, but the standard mandates that the catch block in the constructor rethrow the exception. A more detailed explanation: http://www.gotw.ca/gotw/066.htm
But on further thought, there's still the problem of destructor handling constructor's exception.
Not really. The destructor is only called for objects whose lifetime is at an end. An object's lifetime starts after the constructor ends successfully. If the constructor throws, the lifetime of an object never started, and thus it can't end, so the destructor is not called.If an object holds objects of its own, and one throws halfway during construction, only the destructors for the objects that managed to be constructed will be called, in reverse order of construction.
-
But on further thought, there's still the problem of destructor handling constructor's exception.
The destructor is not called if the constructor throws. Destructors are only called on constructed objects and if the constructor throws the object isn't constructed.
-
TIL.
Also fuck you Discourse, I'm descriptive enough!
-
CAPITAL LETTERS ARE NOT DESCRIPTIVE!
<lalala>
-
Til.
-
YOU STILL LEARNED IT WRONG
<Seriously...>
-
So the only way to get half-initialized object is either placement new,
I don't think this is a fair assessment. Essentially, placement new lets you try to construct an object in an arbitrary position in memory. Meaning you need to create a buffer, and then call placement new on that buffer. This will try to create an object at that position.
If the constructor throws, then you don't have a "half-constructed object". You have a block of memory that didn't hold an object before, and still doesn't hold an object now.
On the plus side, you don't need to call the destructor on that block of memory either (if you create an object with placement new, you are responsible for calling the destructor too).
-
YOU STILL LEARNED IT WRONG
http://iambrony.dget.cc/mlp/gif/deal_with_it_2_by_mezkalito4p-d4hpl02.gif
-
Yeah, you can't catch exceptions in a constructor.
Of course you can, but in doing so you mustn't leave the object in a partially constructed state. This isn't an issue with simple object construction scenarios, but with more complex situations it is clearly something that matters.
For example, consider an object that represents a TCP connection to a remote host where that host is specified by its DNS name, not an unreasonable thing. Failures may happen during the lookup of the name, or during the establishment of the connection (there's a lot of failure modes, especially when you consider that you might need to try connecting with both IPv6 and IPv4). But from the outside, all that you see is you either create an object that is correctly connected, or you get an exception telling you what caused a failure that the code itself couldn't recover from: the caller can avoid having to know much about just how nasty networking really is. And that's the whole damn point.
-
I approve.
-
Of course you can,
How did you manage to focus on that sentence, while missing the sentence above (where I mention I was ninja'd by @nodrefrofr, who posted an excerpt from the C++ standard) and the following sentence that clarified
@Kian said:Or rather, you can catch them, but the standard mandates that the catch block in the constructor rethrow the exception.
I'd consider it pendantry if you had bothered to clarify the situation where what you say makes sense, but you didn't so I'm assuming you ignored the context of the conversation up to this point, which goes over your objection already. So I'll make it more clear.
Yes, the constructor body can throw, and you can catch the exception within the constructor body. This is not interesting, and to an external observer it's the same as if the constructor had not thrown. We were talking about a situation where an exception might leave a half constructed object.
Gaska brought up the rather obscure syntax for catching an error in an initializer list (point to him, I had to look it up), the function try block. What nodrefrofr and I pointed out was that the function try block can catch exceptions, but in the context of constructors and destructors it has to rethrow them. If you omit the
throw
clause, the compiler will just do it for you. Which is why his attempt to build a zombie object failed at runtime.
-
How did you manage to focus on that sentence, while missing the sentence above
I've had that sort of day.
-
-
@Gaska said:
```
FILE* f = fopen("dupa");I see what you did there.</blockquote> Wrote code that doesn't compile?
-
Give me a break. I haven't used fopen() in years.
-
On further thought, how do you know which fopen() I was using? Maybe my fopen() accepts just one argument?
-
Because any alternative would be TR :WTF:?
-
I was curious about how Rust handled object construction, since as you say they use errors. My understanding was that Rust had destructors, but having destructors without exceptions seems to create problems.
So, a quick search says that there's no "constructors" per se, but instead there's a convention to declare static methods that return objects of the correct type. Which raises the question of just how the type is created inside the constructor. I imagine the constructor returns an error or the object, so if you fail you have an error.
Would it be accurate to say that types are simply structures, and you can create one without calling a constructor? Is there a way to say "only create an object of this type through the constructor function"?
I imagine if you create it through the constructor, you have to check that the constructor succeeded (which as you say, raises a compiler warning if you don't). But is there anything that stops you from treating it as if it had succeeded afterwards? That is to say, in pseudo code:
let x = TypeA::constructor(); if (x == error) { doSomething(); } // Can I refer to x here as if constructor had succeeded?
-
My understanding was that Rust had destructors, but having destructors without exceptions seems to create problems.
Yeah, Rust can't signal any errors in destructors. It gets worse when you realize that destructors aren't guaranteed to run. They used to be, but they found a terrible memory safety bug related to it. Mind you, destructors not running is a very rare case - but you can't really depend on it. However, most destructors are there to free memory after object is destroyed, and it isn't that bad to leak some memory if the goal is to never segfault. There is an ongoing work in how to make it possible to explicitly guarantee that on some objects - it turns out to be quite non-trivial. Remember that those guys are a bit crazy about good design a safety - the current hottest topic in the community is that ref-counting pointer is unsafe to use if the ref counter overflows.Would it be accurate to say that types are simply structures, and you can create one without calling a constructor?
Well, yes. Structures in any language are just pieces of memory that you arbitrarily give some specific semantics.The trick is, you need to have all the fields of a struct accessible to create it, so unless they're all declared public, only the module the struct is declared in can directly construct it. If there's at least one private function, the type cannot be constructed otherwise than via a public function that comes from this module and returns an object.
let x = TypeA::constructor();
if (x == error) { doSomething(); }
// Can I refer to x here as if constructor had succeeded?
No, because x holds the error in this case, not the object.BTW, idiomatic name for constructor function is
new
.
-
it isn't that bad to leak some memory if the goal is to never segfault
I wouldn't want the language designer making that decision for my application though.
-
the current hottest topic in the community is that ref-counting pointer is unsafe to use if the ref counter overflows.
Don't they have any bikesheds to paint, instead? A ref-counted pointer should have a counter that is the same size as the pointer itself. If that overflows, it means you have more references than actual memory locations. How is the pointer theoretically supposed to overflow?
No, because x holds the error in this case, not the object.
I understand x holds the error. That's my concern. What x holds is defined at runtime. So can I only access x from within some structure (I mean like an if block here) that checks the type? Is that checked by the compiler?
-
ref-counting pointer is unsafe to use if the ref counter overflows.
How big is the ref-counter?
-
Structures in any language are just pieces of memory that you arbitrarily give some specific semantics.
Yeah, but the semantics in C++ are that you can't create an object except through its constructor. That's pretty different from "only if you mark something as private," as in Rust. It's an odd choice.
Doesn't that mean you can't create something "in place"? It always has to be obtained as the return of a function? What about objects contained inside an object you have to? You have to manually call each of their constructors as well?
-
I wouldn't want the language designer making that decision for my application though.
That's what they're trying to do - give a programmer a way to say "hey, make sure those destructors do run, okay?". But apparently, it's so hard it didn't make into 1.0.Don't they have any bikesheds to paint, instead? A ref-counted pointer should have a counter that is the same size as the pointer itself. If that overflows, it means you have more references than actual memory locations. How is the pointer theoretically supposed to overflow?
Here's the discussion. They use forget() function, the purpose of which is to throw a value out of scope without running a destructor - so at the time you have only a single Rc in memory, but "logically" you have many of them because they're not destroyed. Probably the most corner-case-y corner case imaginable, but still causes segfaults in safe code, which is unforgivable for some reason. They're little weird, those Rust devs.I understand x holds the error. That's my concern. What x holds is defined at runtime. So can I only access x from within some structure (I mean like an if block here) that checks the type? Is that checked by the compiler?
Actually,x
is aResult<TypeA, ErrorA>
. ThisResult
is an enum (a tagged union actually) with two variants - It's eitherOk(o)
, in which case you can accesso
, which is of typeTypeA
, orErr(e)
withe
beingErrorA
. In the latter case, you can't acceso
, because it simply doesn't exist.How big is the ref-counter?
Processor word size, which is 32-bit on x86. Apparently, they're okay with this bug on 64-bit architectures, because creating 264+1Rc
s takes unrealistically long time to happen, but with 32-bit counter it's under a minute.Yeah, but the semantics in C++ are that you can't create an object except through its constructor. That's pretty different from "only if you mark something as private," as in Rust. It's an odd choice.
It's not odd at all. The rule doesn't say "you can't instantiate something if it has private parts and you're not in the same module". The rules say "you must assign a value to all fields on instantiation" and "you can't access private parts from outside the module" - both of them are blatantly obvious and make perfect sense in separation, and when combined, the Rust way to enforce constructors is blatantly obvious and makes perfect sense as well. That's what I like about Rust - they make few language rules, but with very deep impact. Function arguments, local variable declarations,match
statement andif let
s all use the same syntactic elements and follow the same semantical rules.Doesn't that mean you can't create something "in place"?
You can, but only according to the two rules I mentioned above. But then, it is really that different from mandatory constructors in C++?What about objects contained inside an object you have to? You have to manually call each of their constructors as well?
Why, yes. You must do the same in C++. How else do you imagine doing things?
-
They use forget() function, the purpose of which is to throw a value out of scope without running a destructor - so at the time you have only a single Rc in memory, but "logically" you have many of them because they're not destroyed.
So if I get this right, this is a consequence of not being able to guarantee destructors get called?
Actually, x is a Result<TypeA, ErrorA>. This Result is an enum (a tagged union actually) with two variants - It's either Ok(o), in which case you can access o, which is of type TypeA, or Err(e) with e being ErrorA. In the latter case, you can't acces o, because it simply doesn't exist.
I can follow all that until the last sentence. I understand that o doesn't exist at runtime, because the constructor failed.My question is, what prevents me from assuming the constructor succeeded. That is, in this code:
let x = TypeA::new(); // What does the code here look like that I can't refer to the TypeA object in x
Short of the compiler running a bit of static analysis on every such call, which I imagine would add to the compilation time, it shouldn't be detectable.
Sorry to be asking you. I looked here: http://rustbyexample.com/custom_types/enum.html but none of the code there, that I could see, dealt with errors at all.
-
Why, yes. You must do the same in C++. How else do you imagine doing things?
Well, you can obviate calls to default member constructors that take no arguments in your constructor. This way feels like it requires a bit more boilerplate in some situations.
-
So if I get this right, this is a consequence of not being able to guarantee destructors get called?
Yes and no. Yes because if destruction was mandatory, it wouldn't ever overflow without running out of memory. No because forget() is included in standard library because it serves a purpose - there are some legitimate cases where you really don't want to run destructor, so it should be possible to opt-in for it - and then you get this here problem.The discussion about guaranteed destruction is far more interesting than about this bug - partially because it's harder problem, but mostly because it actually matters and the problem is very real. CBA to find it.
My question is, what prevents me from assuming the constructor succeeded.
The value ofx
isErr
and notOk
. Simple as that.Well, you can obviate calls to default member constructors that take no arguments in your constructor. This way feels like it requires a bit more boilerplate in some situations.
That's because in C++ you can have uninitialized variables (they default to default constructor) and in Rust you can't.
-
My question is, what prevents me from assuming the constructor succeeded.
You can't get at either value directly. You can only get at
o
ore
in an appropriatematch
statement, and only the applicable branch (Ok(o)
orError(e)
, depending on whether the call succeeded) will run.
-
The value of x is Err and not Ok. Simple as that.
This doesn't answer my question.
@PleegWat said:You can only get at o or e in an appropriate match statement, and only the applicable branch (Ok(o) or Error(e), depending on whether the call succeeded) will run.
This does.However, if this is so, why can't you guarantee the destructor will always run? Can you point to discussion that explains why you sometimes don't want to run the destructor? You like the elegance of simple rules, and the rule "if the constructor succeeded, the destructor runs" looks pretty simple and elegant. Adding a "sometimes" there is a pretty big cost, and as the ref counted pointer shows, it breaks the RAII idiom.
-
It's a kinda hacky, but very impactful corner case. TBH, it's beyond my understanding why you would want either to happen - but trust me, there are uses.
-
This doesn't answer my question.
This does.
And how was it different from what I said before?
-
And how was it different from what I said before?
You said:This Result is an enum (a tagged union actually) with two variants - It's either Ok(o), in which case you can access o, which is of type TypeA, or Err(e) with e being ErrorA. In the latter case, you can't acces o, because it simply doesn't exist.
What was missing wasYou can only get at o or e in an appropriate match statement,
Which you didn't explain clearly enough for me to understand (perhaps you mentioned it in passing earlier, and took it for granted that I remembered and understood it now). I was asking how the syntax and semantics of the language specifically kept me from the wrong value, and you just told me it did.The experience I have with unions is that you SHOULD only read what you put in, but the syntax lets you access any of the members. So when you say "an enum is a tagged union", that doesn't tell me you can only access the members through a specific construct that checks the tags. It only tells me that there's a tag you can check.
-
The experience I have with unions is that you SHOULD only read what you put in, but the syntax lets you access any of the members.
If your experience is limited to C, then this is understandable - C unions are totally fucked up. But just like with errors, you shouldn't assume that if some feature is totally fucked up in C and doesn't exist in Java, then it must be totally fucked up in every other language implementing it as well. Tagged unions exist in many languages, and almost all of them handle them sanely.I didn't think that you wanted a code sample - I assumed that you either know what matching syntax is in Rust (it doesn't take long to google and understand it), or you don't want to learn Rust and just want to know it on the concept level. When I said "doesn't exist", I meant there's no syntax available to access it, because, well, it doesn't exist. If you never declared
foo
, you can't usefoo
either.
-
It's a kinda hacky, but very impactful corner case.
I think he'd be satisfied with a link to the discussion. Sometimes that's just the easiest way.
-
Yeah, if I could find it. I'm not really a bookmarking person. I'm sure he can find it himself if he's determined enough.
-
I'm not really a bookmarking person.
Nor am I; the Googlezzzzz are effective, but you know better what you're talking about than he does.
-
>How big is the ref-counter?
Processor word size, which is 32-bit on x86. Apparently, they're okay with this bug on 64-bit architectures, because creating 264+1 Rcs takes unrealistically long time to happen, but with 32-bit counter it's under a minute.
Hmmm....wow...how do they create that many without running OOM?