A problem with big numbers



  • @tarunik said:

    C++11 or C++14

    But the C++98 definition of a POD is so much more entertaining!


  • Banned

    @tar said:

    So you're suggesting the change request originated outside of engineering? That seems valid.

    No, I'm suggesting most programmers are bad at what they're doing.

    @tar said:

    Their main advantage is that you can use C-style idioms with them, and pretend it's still 1990 in your codebase, which I'm sure will delight your coworkers.

    Also, optimizations. A very big deal in gamedev.

    @tarunik said:

    C++11 or C++14 (for the former, I use the N3337 draft as a proxy)

    Here, have this updated draft N3797. You probably want both, since N3337 is C++11, and this is C++14.



  • @Gaska said:

    Also, optimizations. A very big deal in gamedev.

    Well I might want to press you on that point: which specific optimizations?

    (Also, are you a graphics programmer? ;)


  • Banned

    @tar said:

    Well I might want to press you on that point: which specific optimizations?

    Skipping initialization, memcpy()-ability without restrictions (including dumping to file and reading back), return value optimization, more aggressive control flow analysis due to no side effects of moving things around. I don't have intimate knowledge of compilers, so only the most basic stuff here.

    @tar said:

    (Also, are you a graphics programmer?

    Not really, but I know quite a bit about it.



  • @Gaska said:

    Skipping initialization, memcpy()-ability without restrictions (including dumping to file and reading back), return value optimization, more aggressive control flow analysis due to no side effects of moving things around. I don't have intimate knowledge of compilers, so only the most basic stuff here.

    Skipping initialization may be valid in some cases, but I'd be surprised if a modern compiler set to aggressive enough settings can't infer enough about your code to apply the other optimizations for you. I may try and find the time to write up some test code to prove it either way.


  • Banned

    @tar said:

    I'd be surprised if a modern compiler set to aggressive enough settings can't infer enough about your code to apply the other optimizations for you.

    The biggest obstacle is that not calling constructors changes semantics of code - and optimizations shouldn't do that. For the same reason, if you have x < sqrt(y) in loop condition, sqrt(y) doesn't get extracted in front of the loop.



  • @Gaska said:

    sqrt(y) doesn't get extracted in front of the loop.

    ...presuming that y doesn't change over the loop?



  • @Gaska said:

    The biggest obstacle is that not calling constructors changes semantics of code - and optimizations shouldn't do that

    Optimizations shouldn't change the semantics, agreed. Empty ctor should have no effect though...



  • @tar said:

    ...presuming that y doesn't change over the loop?

    Presuming sqrt() has no side effects, which is arguably a harder thing to prove. Most compilers (by which I mean GCC) have a way to mark a function as pure or something like that, but I don't think it would be easy to deduce just by code analysis.


  • Banned

    @tar said:

    Optimizations shouldn't change the semantics, agreed. Empty ctor should have no effect though...

    But it might be in different compilation unit, so compiler can't check if it's empty (it might at link time.

    @Maciejasjmj said:

    Presuming sqrt() has no side effects, which is arguably a harder thing to prove

    The thing is, it has side effects. There's a flag for unsafe maths which fixes that, though.



  • @Gaska said:

    But it might be in different compilation unit, so compiler can't check if it's empty (it might at link time.

    Are we counting having the empty ctor defined inline in a header file as an optimization?


  • Banned

    Inlining always is optimization. Also, remember that empty constructor doesn't automatically mean no-op constructor - members' constructors are called too.


  • Discourse touched me in a no-no place

    @tar said:

    "Help! I added a virtual [dtor] function to my class, and my code is crashing really strangely now!"

    My C++ with regard to this is rusty - care to explain? For example - is this wrong?

    class base {
    //...
       virtual ~base() { /*... */ }
    }
    class derived: public base{
        virtual ~derived() { /* non-trivial resourse deallocation */ }
    }
    
    base* foo = new derived();
    //...
    
    delete foo;
    

    Inadvertent syntax errors aside, wouldn't removing virtual in the above result in a leak?



  • This is a different type of bug.

    The bug is in the use of the direct memory access in a way that is only valid for a POD.

    Adding any virtual function means the class isn't a POD anymore, so the line memcpy(this, &other, sizeof(*this); can (or perhaps will) blow away the vtable.

    [size=10]Guessing that memset() was a typo for memcpy(). Don't think filling the object with one byte of &other was intended.[/size]



  • The problem is that when you have a POD type, you can use C idioms on it, such as using memset(this, sizeof(*this), 0) to initialize it's memory to zero, and other such hackery. When you add a virtual method to a class (be it a dtor or any other member), instances of the object gain a pointer to the vtable for the class, so that the proper behavior can be selected at runtime. If you then perform the previous memory hackery, you null your vtable pointer, which has bad consequences when you attempt to use the virtual method.


  • Discourse touched me in a no-no place

    @Kian said:

    POD type,

    This is what I think I missed...



  • Removing virtual from the base dtor in that example would result in a leak, but that code certainly seems safe enough otherwise.



  • @lightsoff said:

    Guessing that memset() was a typo for memcpy(). Don't think filling the object with one byte of &other was intended.

    Now I think about it a bit more, the problematic code was in a default ctor, not a copy ctor: the guy was actually calling memset(this, 0, sizeof(*this)) on his object, blanking out the vtable, and then having to deal with the fallout the moment he tried to call a virtual function on his mutilated object...



  • Another "great" C++ bug I had to deal with (self-inflicted this time), was when I had a set of classes set up which were being used between two threads, and I somehow arranged to have the base class destructor be threadsafe, but none of the derived class destructors.

    So one thread would be happily interacting with one of these objects, when the other thread decided that it wasn't needed any more and call delete on it, which would immediately destroy the derived portion of the object, and then enter the base dtor and block due to the first thread owning the mutex. But if the first thread tried to call a virtual function now, it would trigger a "pure virtual call exception" due to the general state of the obect's vtable at that moment, which is an odd thing to have happen in the normal course of things...

    Took me a week or so to figure that one out, had a bit of a :headdesk: moment when I finally understood what the bug was...

    Consider replying to several posts at once



  • Ow.

    My "favourite" so far was a crash in a worker thread where the debugger somehow didn't manage to stop the other threads fast enough, so half of the application had deleted itself before the debugger could show me anything.

    At least, that's what I think was happening, based on the fact that several (though not all) static const objects had somehow got deleted by the time it broke into the debugger.



  • @tar said:

    So one thread would be happily interacting with one of these objects, when the other thread decided that it wasn't needed any more and call delete on it, which would immediately destroy the derived portion of the object, and then enter the base dtor and block due to the first thread owning the mutex.
    It seems like this isn't so much a threading issue as it is an object lifetime issue. Your logic is trying to kill an object that is being used by another thread, and making the destructor thread safe doesn't solve that.

    In general, destructors don't need to be thread safe because only one thread should ever call them, and only when no one else has a reference to the object being deleted. It might help to wrap such objects in a shared_ptr or other such reference counted wrapper so that the destructor is only called once, when all references are dead (std::shared_ptr is already thread safe, so you don't need to worry about races on the reference count).



  • @Kian said:

    In general, destructors don't need to be thread safe because only one thread should ever call them

    Only thread B would ever call the dtor in my example above. But it wasn't waiting for thread A to finish doing other stuff with the object before (partially) deleting it

    In the end, I gave the base class its own special operator delete and put the mutex in there, then made the dtors of all the classes private.

    It was a pre-C++11, boost-free codebase FWIW, so no fancy smart pointers to be had...



  • It's undefined behavior. Practically speaking probably you'll only get a leak, but hypothetically you could have more go wrong.

    To make up a somewhat-plausible scenario off the top of my head, I think the following would be a legal line of reasoning:

    • With base* foo, delete foo is only legal if foo actually points to a base object.1
    • At the point of creation, the concrete type being created is also known for obvious reasons.
    • Thus for types with non-virtual destructors, in legal programs the concrete type is known at both the new and delete location. The compiler can then use a separate object store for those types! It might, for example, have a separate heap for objects of a few different, fixed, sizes, so that allocations do not have the overhead of storing the block size. (Sort of a compile-time version of the behavior that many malloc implementations do dynamically.)

    If you had such an implementation that ran on PJH's example:

    • Because derived has a virtual destructor, the compiler would not be able to make that optimization and would have to allocate new derived() on the traditional heap. (Or, if you removed virtual from ~derived, then derived would be a different fixed size and could be put into a third pool.)
    • At delete foo, the compiler would assume foo's dynamic type is base because that's the only thing it legally can be, and thus treat foo as if it were in the separate store.
    • Memory corruption ensues!

    (Now, an actual implementation would be able to check the value of foo to make sure it falls into the right heap range or something like that. The point is that the standard wouldn't require it. And I think that the most likely implementation would be able to behave "correctly" (just a leak) in the face of a fixed-size/general-heap confusion, but would not behave correctly in the face of confusion between two different fixed sizes.)

    1 "In the first alternative (delete object), if the static type of the object to be deleted is different from its dynamic type, the static type shall be a base class of the dynamic type of the object to be deleted and the static type shall have a virtual destructor or the behavior is undefined." N3690, §5.3.5 para 3, emphasis mine. The same sentence is present in the same location of the C++03 final standard.



  • so I should s/a leak/nasal demons/ in my initial response?

    My point was basically: yes, don't delete the virtual. Standard seems to agree?



  • @tar said:

    so I should s/a leak/nasal demons/ in my initial response?
    Yes.

    I interpreted "it seems safe otherwise" to mean "other than the leak, things will work correctly", which is why I responded as a did. I now suspect I was wrong, and you meant it to mean "if you don't remove virtual, it is safe"; that is also true.

    In other words, as written, it is correct. Without virtual, it is not. :-)



  • @EvanED said:

    In other words, as written, it is correct. Without virtual, it is not.

    I think we are in agreement, but you probably meant ^^this. :<weerq>D



  • Thanks; fixed. :-)



  • @EvanED said:

    To make up a somewhat-plausible scenario

    That's not needed. Basically, gcc's optimizer is a bitch. When it notices a piece of code with Undefined Behaviour™, it gets mean and generates a bit of code that does something totally nonsensical and unrelated to the bug you had in it's stead.

    For those who don't believe in sentient optimizers (I don't myself either), the explanation seems to be that the optimizations are based on some very subtle assumptions the specification allows and when those assumptions are broken by presence of Undefined Behaviour™ the thing breaks down and reorders statements in totally unexpected ways or even forgets some or something like that. In any case I've seen couple of crashes or odd behaviour that disappeared when I fixed something seemingly unrelated that I never understood how it could ever have caused what was happening.

    By the way, deleting object via base-class pointer without virtual destructor can crash for much simpler reason—whenever the base subobject does not start at the same address as the final object, the delete operator won't be able to calculate the correct pointer to pass to the underlying free. And that can happen when the base is not leftmost ancestor, but I believe I've seen it happen just because the base did not have any virtual members, the subclass did and the compiler allocated the virtual method table before the base (it can; layout is only defined for standard layout objects and objects with virtual members are not that).


  • Java Dev

    I don't recall where (possibly an llvm blog) but I read once that when a certain codepath includes undefined behaviour, it is valid for the optimizer to assume that codepath will never run. If it's an if branch, that means by extension that its conditional can be assumed to be true (or false, if the UB is in the else). This may bubble on quite a bit in flow analysis.



  • That rings a bell.

    Though I would prefer the optimiser to say "Stop you fool!" and refuse to compile anything with known undefined behaviour.

    Not always possible though.


  • Java Dev

    Yup, the undefined behaviour may be hidden behind one or more layers of macros. The optimization may well be desired.

    I think the blog post I mentioned is http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html



  • That simply isn't possible. For example, if you have a function that takes a pointer, and you don't guard against it being null before dereferencing it, the compiler would have to refuse to compile that function. Even if you know that you'll never pass a null to that function and so using it without checking is ok.

    The same is true of other instances. Some statements may invoke undefined behavior if executed with certain parameters and not others. There's no way for the compiler to know if you'll ever use parameters that would invoke undefined behavior. So it simply assumes that it's impossible, since if it gets it wrong it means you invoked undefined behavior and it is thus allowed to do whatever it wants anyway.


  • Banned

    Fun fact: due to the above, if you write a null check but later (or earlier) dereference the pointer regardless of it, gcc will remove the null check. This was a cause of really nasty bug in Linux kernel once.



  • @PleegWat said:

    when a certain codepath includes undefined behaviour, it is valid for the optimizer to assume that codepath will never run

    When a certain codepath includes undefined behaviour, it is valid for the optimizer whatever the hell it pleases, including assuming it will never run. But that's not how it works.

    The whole point of declaring some code Undefined™ is that:

    • defining it would prevent useful optimizations,
    • it is not very useful, and
    • it is difficult to recognize it statically.

    The optimizer does not know that the codepath contains undefined behaviour. It assumes it does not and because it's assumptions are not satisfied, the algorithm computes something the programmer has hard time understanding.



  • @Kian said:

    So it simply assumes that it's impossible

    … even if you are provably doing just that. That often happens with strict aliasing. Even within a single function when you type-pun a point and dereference it in a way that violates strict aliasing, gcc will mark the operations via the pointer as independent (though the dependency is right there in plain sight) from those on the pointee and reorder the writes in any random nonsensical order.



  • @Bulb said:

    the explanation seems to be that the optimizations are based on some very subtle assumptions the specification allows and when those assumptions are broken by presence of Undefined Behaviour™ the thing breaks down and reorders statements in totally unexpected ways or even forgets some or something like that

    Kinda, sorta.

    Basically, a compiler can treat the whole codepath involving undefined behavior as unreachable and optimize based on that. Including propagating the optimization backwards.


  • FoxDev

    @Gaska said:

    Fun fact: due to the above, if you write a null check but later (or earlier) dereference the pointer regardless of it, gcc will remove the null check

    sounds like valid nasal demons territory there.

    i see no problems here. any decent linter should have found that anyway. (i mean the specific case of dereferencing a pointer without a null check)



  • @PleegWat said:

    possibly an llvm blog

    What Every C Programmer Should Know About Undefined Behavior #1/3 #2/3 #3/3

    There, that should keep y'all busy for a while...

    I like to call this [b]"Why undefined behavior is often a scary and terrible thing for C programmers"[/b]. :-)

    (ETA: I see you found it as well... :<qx>D)



  • @Bulb said:

    That's not needed.
    I know it's not _need_ed. A compiler could have code specifically to detect such cases and then, instead of either warning or generating code that will work anyway, generate code that will send all your porn to your mom.

    But in reality, compiler optimizations are applied for a reason other than spite, and I was giving a somewhat plausible reason for why it might help and what mechanism it would break.



  • @Maciejasjmj said:

    Basically, a compiler can treat the whole codepath involving undefined behavior as unreachable and optimize based on that. Including propagating the optimization backwards.

    And that can get you into actual trouble if the compiler decides to remove a NULL check becuase you've already dereferenced a pointer...



  • @accalia said:

    sounds like valid nasal demons territory there.

    Well, the problem is that kernel needs to access all addresses, which sometimes includes address with numeric value 0.

    And since kernel is non-hosted environment and Linux kernel is written in GNU C, the specification is not really relevant.

    @EvanED said:

    But in reality, compiler optimizations are applied for a reason other than spite, and I was giving a somewhat plausible reason for why it might help and what mechanism it would break.

    Of course. What I am saying is that most of the optimizations are heavy wizardry or outright black magic and tend to handle undefined behaviour in completely incomprehensible ways.

    Besides I then provided a simpler example how it can go wrong ;-)



  • @Bulb said:

    most of the optimizations are heavy wizardry or outright black magic and tend to handle undefined behaviour in completely incomprehensible ways

    There's a list of a few more fun ways that UB can attack you here, along with this challenge:

    I will send a small but nice prize to the person who posts a comment describing the most interesting, surprising, or just plain crazy object code emitted by a compiler as a consequence of undefined behavior.

    So the comments may be interesting, although there's no obvious declaration of a "winner" that I saw yet...



  • @tar said:

    ...although there's no obvious declaration of a "winner" that I saw yet...

    The top of that post says "UPDATE: Winners are here" :-)



  • Ah, must've glossed over that! :<dds>) Will read tomorrow!


Log in to reply