1. 13
  1. 6

    We discussed reply to this paper here.

    1. 3

      I support standardizing -fno-strict-overflow and -fno-strict-aliasing in ISO C. This is basically status quo, and standardizing existing practice is usually a good idea. On the other hand, I am pretty skeptical about proposals along the line of platform-specific UB. I am yet to see any good proposal, and most proposals can be described as compiler user wishlist with no connection to compiler implementer reality.

      1. 1

        I disagree with standardizing the no-overflow/no-strict-aliasing flags. Using these options is not standard practice (it may be reasonably common practice, but that’s not the same thing). Supporting these options (or equivalents) is pretty standard in compilers, but the standard already allows for that (it doesn’t mandate it).

        The point of standardising the language is so that it is clear what the semantics are, and what is and what is not allowed, so you can have some assurance about the behaviour of code even when compiled using different compilers. That assurance is significantly reduced if you now need to know the specific variant of the language the code is written in. I can foresee problems arising where “no-strict-overflow, no-strict-aliasing” code would be unwittingly used in the wrong (strict-overflow, strict-aliasing) context and be broken as a result. It would arguably be better to not have these options at all, since their presence leads to their use, and their use allows what is fundamentally incorrect code to be written and used. I would much rather see standardised consistent solutions that would be embodied within the source: special “no strict overflow” integer types (or type attributes), “may alias all” pointer types, and so on. And we sorely need simple, consistent and standard ways to safely check for overflow before it happens (such as what GCC provides, but which of course is not standard: https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html).

        1. 1

          For implementation strategy, I think standardized pragma would be best, because as you pointed out, flag risks use in wrong context.

          what is fundamentally incorrect code to be written and used

          This is… non sequitur? Incorrect according to whom? It is only incorrect according to the current standard, which is immaterial since the proposal is precisely to update the standard. This is like replying to “software should not be patentable” by “infringing software patent is illegal”, both true and useless. This is not about being correct or incorrect. -fno-strict-overflow and -fno-strict-aliasing are useful. Linux kernel uses them!

          “no strict overflow” integer types, “no strict aliasing” pointer types, overflow checking builtins

          All good ideas, but this is also perfect embodiment of “perfect is the enemy of good”. These are not existing practices (okay, except GCC overflow checking builtins, I support standardizing them yesterday), unlike -fno-strict-overflow and -fno-strict-aliasing flags. Considerable design work needs to be done, and prototype needs to be field tested. We should start that today, but standardization is far off.

          1. 1

            This is… non sequitur?

            No, I don’t think so.

            Incorrect according to whom? It is only incorrect according to the current standard


            which is immaterial since the proposal is precisely to update the standard

            That’s the proposal you are making, but not one I agreed with, for the specific reason that I don’t want “standard C” to actually be multiple different languages. The existence of these options (outside the standard) and the fact that they lead to multiple variants of the language (standardised or not) is very relevant, not immaterial at all.

            This is like replying to “software should not be patentable” by “infringing software patent is illegal”

            The argument is “these options should not be standardised, because they cause problems”.

            1. 1

              I understand concerns about fragmenting the language, but my view is that it is already lost. Linux kernel exists, and C is already fragmented in practice.

              I am not proposing this, but one way to solve fragmentation is to standardize -fno-strict-overflow and -fno-strict-aliasing behavior, without any option. If you want lost optimization you can add flags yourself, exactly as you can add -ffast-math now.

              1. 1

                I understand concerns about fragmenting the language, but my view is that it is already lost. Linux kernel exists, and C is already fragmented in practice.

                I agree it’s already a problem. I think the underlying causes for this should be addressed, but not by standardising the language variants that have already emerged (nor by removing strict aliasing / strict overflow altogether).

            2. 1

              All good ideas, but this is also perfect embodiment of “perfect is the enemy of good”. These are not existing practices

              I also disagree with this characterisation, regardless of whether they are existing practices.

              But in fact: https://gcc.gnu.org/onlinedocs/gcc-4.0.2/gcc/Type-Attributes.html

              • “may_alias” attribute (applies to pointee types, not pointers themselves, so not exactly what I suggested, but close enough).

              There’s no similar attribute for wrap-on-overflow, unfortunately. But I don’t think “it hasn’t been done, therefore it should not be done” is an argument that really holds water. And characterising it as “the perfect” because it hasn’t been done seems a stretch. (edit: and characterising your own proposal as “the good” is begging the question).

              1. 1

                it hasn’t been done, therefore it should not be done

                What the fuck. I said “We should start that today”. I am just pointing out that “put it on types” has had less field testing than “put it on flags”.

                1. 1

                  Sorry, I missed your “we should start that today” comment and I didn’t intend to anger you.

                  this is also perfect embodiment of “perfect is the enemy of good”

                  I interpreted the above other than “we shouldn’t do what you have proposed (the perfect) because there is a solution that I have proposed (the good) that is easier because it has already been done”. Now, I think what you meant was actually “we should do what I have proposed now rather than delaying indefinitely until we can do something better”. I’m afraid I still disagree; I don’t want to see these language fragments standardised. On a practical level, I also think it’s unlikely either change would be standardised in any short time frame. The ISO C committee is not known for, err, actually correcting significant problems in the language and its specification.

            3. 1

              not standard

              It has been proposed.

          2. 3

            Consider a simple scalar read *x = y. In K&R C, a programmer could be reasonably certain that code would be generated to copy a value from the storage starting at the location of y to the location with address x, something like load x,Reg1; load (y),Reg2; store Reg2,(Reg1) or perhaps simpler if some values are already cached in registers.

            An argument so speciously simple makes me doubt the conclusions of this paper. Consider *x = y; *x = y;. Would you expect it to generate the same assembly twice? No, and people will shit on your compiler if it does. If values are already cached in registers as it describes then that code could shorten to load x,Reg1; load (y),Reg2; store Reg2,(Reg1); store Reg2,(Reg1);, ok. But then if we know that duplicate stores can be removed it will be even better, so the second assignment can be removed completely. But then if you do z = x; *x = y; *z = y; then what happens? Boom, you have to know about aliasing pointers, which is what most of the rest of the paper complains about. It’s definitely a hard problem and the C standard does a shit job of dealing with it, but a programmer who can be “reasonably certain” that the compiler actually does what they write is a programmer who will write a lot of obfusticated BS code because the compiler can’t be trusted to do its damn job in even the easiest cases.

            But at worst it contains a good overview of the problems and lots of good references to other arguments!

            1. 2

              There’s so many issues that come from the need to optimise that I wonder if C could solve a few problems by introducing “don’t touch this” blocks. Basically “volatile”, but for lines of code where no optimisation takes place, no dereference is skipped, no overflows are analysed, etc. So you’d write:

              volatile { *foo = bar[a+b]; }

              and whatever else is happening around that block, you’d do the addition, deref bar, load the foo address and write there - no changes allowed.

              Given how much analysis and workarounds we’re already stacking here, wouldn’t handing the control back to the dev be simpler at this point? (This would probably need to disable LTO though)

              1. 5

                The root problem is that people want C to be two things:

                • A portable assembler.
                • A language with compiler optimisations.

                You can’t have both in a single language. If you want a trivial-to-understand lowering from your source syntax to the sequence of executed instructions then you can’t do any non-trivial optimisations. You can do constant propagation. You might be able to do common-subexpression elimination (though not if you have shared-memory parallelism). That’s about it.

                If you want optimisation then those optimisations will be correct according to some abstract machine. You need to understand what that abstract machine is and accept that your code describes a space of possible behaviours and that any of the behaviours allowed by the abstract machine may occur. The more flexibility the abstract machine allows, the larger the set of possible optimisations. If you want things like autovectorisation of loops then you need to have a memory model that allows the compiler to make assumptions about non-aliasing and happens-before relationships: if partial results to four loop iterations are visible in memory then this would violate the semantics of a very close-to-the-ISA abstract machine, but is fine in C/C++ because the memory model tells you that no atomic has happened that established a happens-before relationship and so the exact order that these things appear in memory is undefined.

                Personally, I’d love to work on a good language for writing operating systems and language runtimes. Something that had a memory model that let you reason about behaviour of lockless data structures and that had a mechanism for me to define my own type-punning rules in the language (such that I could implement a memory allocator and expose the explicit point at which the type associated with underlying memory changed). There are probably a dozen or so projects that would adopt such a language, so it’s hard to justify spending time on it.

                1. 1

                  This would be bitch to specify, but yes, I would like to see a serious try of this. Can a and b be on register, or should compiler be required to load them from stack, for example? You basically need to specify compilation algorithm, which amounts to re-implementing a C compiler. By the way, HTML5 parsing specification does work that way, so such standard can be valuable. It’s just a lot of work and very different style of standardization.

                  1. 1

                    I’m not super familiar with the C standard - why do you think the whole compiler would have to be redefined rather than adding qualifiers like “this transformation may be done here - unless it’s a volatile block”, “this is undefined - unless it’s a volatile block where …”, etc. ?

                    1. 1

                      The C standard doesn’t directly specify transforms that can be applied, at all (maybe one or two very minor exceptions). The extent to which permissible optimisations are specified is mainly via two concepts:

                      The “as-if” rule, which says (more or less) that as long as the observable behavior of a program is correct then the compiler has its job (i.e. it doesn’t matter what code is generated, as long as it produces the output that it should have, according to the semantics of the “abstract machine”). Quote from the standard:

                      The semantic descriptions in this International Standard describe the behavior of an abstract machine in which issues of optimization are irrelevant.

                      Then, there’s the “undefined behaviour” concept, which says (again - roughly) that if a program violates certain constraints, the semantics of that abstract machine are not specified at all. This notion is particularly useful for compilers to exploit in order to enable optimisations. But the standard doesn’t generally talk about actual transformations to the program.

                      That leaves your second point:

                      “this is undefined - unless it’s a volatile block where …”

                      That could be done, to some extent; but then the behaviour (inside such a block) would have to be specified. It’s hard to explain why this is difficult without going into a lot of detail, but suffice to say, the standard is already sufficiently vague in enough areas that it’s already difficult to tell in some cases whether certain things have defined behaviour (and if they do, what it is). Getting the details right for such a change would be very finicky. However, ultimately, what you suggest could probably be done - it would just need a lot of work. I don’t think it would in fact require specifying the whole compilation algorithm.

                  2. 1

                    How would it work with, lets say, function call boundaries? In particular inline functions.

                    inline void write_byte(uint8_t *p, uint8_t v) { *p = v; *p = v; }
                    volatile {
                        write_byte(p, 42);
                        write_byte(p, 64);

                    Should the above write to *p once, twice, or four times? I think twice seems the most reasonable, but I think there are arguments to be made for four writes as well, depending on whether or not write_byte is static inline or not.

                    1. 1

                      work with, lets say, function call boundaries?

                      They don’t have to be allowed inside. I imagine using the volatile block for just a few lines like the inside of write_byte + preventing reordering around that block. Basically a high-level asm block.