Is the strict aliasing rule incorrectly specified?

前端 未结 9 538
野的像风
野的像风 2020-12-29 03:50

As previously established, a union of the form

union some_union {
    type_a member_a;
    type_b member_b;
    ...
};

with n memb

相关标签:
9条回答
  • 2020-12-29 04:01

    Prior to the C89 Standard, the vast majority of implementations defined the behavior of write-dereferencing to pointer of a particular type as setting the bits of the underlying storage in the fashion defined for that type, and defined the behavior of read-dereferencing a pointer of a particular type as reading the bits of the underlying storage in the fashion defined for that type. While such abilities would not have been useful on all implementations, there were many implementations where the performance of hot loops could be greatly improved by e.g. using 32-bit loads and stores to operate on groups of four bytes at once. Further, on many such implementations, supporting such behaviors didn't cost anything.

    The authors of the C89 Standard state that one of their objectives was to avoid irreparably breaking existing code, and there are two fundamental ways the rules could have been interpreted consistent with that:

    1. The C89 rules could have been intended to be applicable only in the cases similar to the one given in the rationale (accessing an object with declared type both directly via that type and indirectly via pointer), and where compilers would not have reason to expect that lvalues are related. Keeping track for each variable whether it is currently cached in a register is pretty simple, and being able to keep such variables in registers while accessing pointers of other types is a simple and useful optimization and would not preclude support for code which uses the more common type punning patterns (having a compiler interpret a float* to int* cast as necessitating a flush of any register-cached float values is simple and straightforward; such casts are rare enough that such an approach would be unlikely to adversely affect performance).

    2. Given that the Standard is generally agnostic with regard to what makes a good-quality implementation for a given platform, the rules could be interpreted as allowing implementations to break code which uses type punning in ways that would be both useful and obvious, without suggesting that good quality implementations shouldn't try to avoid doing so.

    If the Standard defines a practical way of allowing in-place type punning which is not in any way significantly inferior to other approaches, then approaches other than the defined way might reasonably be regarded as deprecated. If no Standard-defined means exists, then quality implementations for platforms where type punning is necessary to achieve good performance should endeavor to efficiently support common patterns on those platforms whether or not the Standard requires them to do so.

    Unfortunately, the lack of clarity as to what the Standard requires has resulted in a situation where some people regard as deprecated constructs for which no replacements exist. Having the existence of a complete union type definition involving two primitive types be interpreted as an indication that any access via pointer of one type should be regarded as a likely access to the other would make it possible to adjust programs which rely upon in-place type punning to do so without Undefined Behavior--something which is not achievable any other practical way given the present Standard. Unfortunately, such an interpretation would also limit many optimizations in the 99% of cases where they would be harmless, thus making it impossible for compilers which interpret the Standard that way to run existing code as efficiently as would otherwise be possible.

    As to whether the rule is correctly specified, that would depend upon what it is supposed to mean. Multiple reasonable interpretations are possible, but combining them yields some rather unreasonable results.

    PS--the only interpretation of the rules regarding pointer-comparisons and memcpy that would make sense without giving the term "object" a meaning different from its meaning in the aliasing rules would suggest that no allocated region can be used to hold more than a single kind of object. While some kinds of code might be able to abide such a restriction, it would make it impossible for programs to use their own memory management logic to recycle storage without excessive numbers of malloc/free calls. The authors of the Standard may have intended to say that implementations are not required to let programmers create a large region and partition it into smaller mixed-type chunks themselves, but that doesn't mean that they intended general-purpose implementations would fail to do so.

    0 讨论(0)
  • 2020-12-29 04:04

    Starting with your example:

    int strict_aliasing_example(int *i, float *f)
    {
        *i = 1;
        *f = 1.0;
        return (*i);
    }
    

    Let's first acknowledge that, in the absence of any unions, this would violate the strict aliasing rule if i and f both point to the same object; assuming the object has no effective type, then *i = 1 sets the effective type to int and *f = 1.0 then sets it to float, and the final return (*i) then accesses an object with effective type of float via an lvalue of type int, which is clearly not allowed.

    The question is about whether this would still amount to a strict-aliasing violation if both i and f point to members of the same union. On union member access via the "." member access operator, the specification says (6.5.2.3):

    A postfix expression followed by the . operator and an identifier designates a member of a structure or union object. The value is that of the named member (95) and is an lvalue if the first expression is an lvalue.

    The footnote 95 referred to in above says:

    If the member used to read the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called ‘‘type punning’’). This might be a trap representation.

    This is clearly intended to allow type punning via a union, but it should be noted that (1) footnotes are non-normative, that is, they are not supposed to proscribe behaviour, but rather they should clarify the intention of some part of the text in accordance with the rest of the specification, and (2) this allowance for type punning via a union is deemed by compiler vendors as applying only for access via the union member access operator - since otherwise strict aliasing is pretty meaningless, since just about any potentially aliasing accesses could also be potentially members of the same union.

    Your example stores via a pointer to a non-existing or at least non-active union member, and thereby either commits a strict aliasing violation (since it accesses the member that is active using an lvalue of unsuitable type) or uses an lvalue which does not denote an object (since the object corresponding to the non-active member doesn't exist) - it could be argued either way and the standard is not particularly clear, but either interpretation means that your example has undefined behaviour.

    (I might add that I can not see how the footnote allowing type-punning via a union describes behavior that is otherwise inherent in the specification - that is, it seems to break the ISO rule of not proscribing behaviour; nothing else in the specification seems to make any allowance for type punning via a union. Furthermore it is something of a stretch to read the normative text as requiring that this form of type punning requires that access must be done immediately via the union type).

    There is often confusion caused by another part of the specification, however, also in 6.5.2.3:

    One special guarantee is made in order to simplify the use of unions: if a union contains several structures that share a common initial sequence (see below), and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them anywhere that a declaration of the completed type of the union is visible.

    Although this does not apply to your example since there is no common initial sequence, I've seen people read this as being a general rule for governing type punning (at least when a common initial sequence is involved); they believe that it implies that it should be possible to use such type punning using two pointers to different union members whenever the complete union declaration is visible (since words to that effect appear in the paragraph quoted above). However, I would point out that the paragraph above still only applies to union member access via the "." operator. The problem with reconciling this understanding is, in that case, that the complete union declaration must anyway be visible, since otherwise you would not be able to refer to the union members. I think that it is this glitch in the wording, combined with similarly bad wording in Example 3 (The following is not a valid fragment (because the union type is not visible ...), when union visibility is not really the deciding factor), that makes some people construe that the common-initial-sequence exception is intended to apply globally, not just for member access via the "." operator, as an exception to the strict aliasing rule; and, having come to this conclusion, a reader might then interpret the footnote regarding type punning to apply globally also, and some do: see the discussion on this GCC bug for example (note that the bug has been in SUSPENDED state for a long time).

    (Incidentally, I am aware of several compilers that do not implement the "global common initial sequence" rule. I am not specifically aware of any compilers which implement the "global common initial sequence" rule while not also allowing arbitrary type punning, but that doesn't mean such compilers don't exist. The committee response to Defect Report 257 suggests that they intend the rule to be global, however, I personally think the idea that the mere visibility of a type should change the semantics of code which doesn't refer to that type is deeply flawed, and I know others agree).

    At this point you could well question how reading a non-active union member via the member-access operator doesn't violate strict aliasing, if doing the same via a pointer does so. This is again an area where the specification is somewhat hazy; the key is perhaps in deciding which lvalue is responsible for the access. For instance, if a union object u has a member a and I read it via the expression u.a, then we could interpret this as either an access of the member object (a) or as merely an access of the union object (u) which the member value is then extracted from. In the latter case, there is no aliasing violation since it is specifically allowed to access an object (i.e. the active member object) via an lvalue of aggregate type containing a suitable member (6.5¶7). Indeed, the definition of the member access operator in 6.5.2.3 does support this interpretation, if somewhat weakly: the value is that of the named member - while it is potentially an lvalue, it is not necessary to access the object referred to by that lvalue in order to obtain the value of the member, and so strict aliasing violation is avoided. But this is again stretching a little.

    (To me it seems under-specified, generally, just when an object has "its stored value accessed ... by an lvalue expression" as per 6.5¶7; we can of course make a reasonable determination for ourselves, but then we must be careful to allow for type-punning via unions as per above, or otherwise be willing to disregard footnote 95. Despite the often unnecessary verbiage, the specification is sometimes lacking in necessary detail).

    Arguments about union semantics invariably refer to DR 236 at some point. Indeed, your example code is superficially very similar to the code in that Defect Report. I would note that:

    1. "Committee believes that Example 2 violates the aliasing rules in 6.5 paragraph 7" - this doesn't contradict my reasoning above;
    2. "In order to not violate the rules, function f in example should be written as" - this supports my reasoning above; you must use the union object (and the "." operator) to change the active member type, otherwise you are accessing a non-existent member (since the union can contain only one member at a time);
    3. The example in DR 236 is not about type-punning. It is about whether it is ok to assign to a non-active union member via a pointer to that member. The code in question is subtly different to that in the question here, since it does not attempt to access the "original" union member again after writing to the second member. Thus, despite the structural similarity in the example code, the Defect Report is largely unrelated to your question.
    4. The Committee Response in DR 236 claims that "Both programs invoke undefined behavior". This however is not supported by the discussion, which shows only that Example 2 invokes undefined behaviour. I believe the response is erroneous.
    0 讨论(0)
  • 2020-12-29 04:07

    The C11 standard (§6.5.2.3.9 EXAMPLE 3) has following example:

    The following is not a valid fragment (because the union type is not visible within function f):

     struct t1 { int m; };
     struct t2 { int m; };
     int f(struct t1 *p1, struct t2 *p2)
     {
           if (p1->m < 0)
                   p2->m = -p2->m;
           return p1->m;
     }
     int g()
     {
           union {
                   struct t1 s1;
                   struct t2 s2;
           } u;
           /* ... */
           return f(&u.s1, &u.s2);
     }
    

    But I can't find more clarification on this.

    0 讨论(0)
  • 2020-12-29 04:07

    The strict aliasing rule forbids access to the same object by two pointers that do not have compatible types, unless one is a pointer to a character type:

    7 An object shall have its stored value accessed only by an lvalue expression that has one of the following types:88)

    • a type compatible with the effective type of the object,
    • a qualified version of a type compatible with the effective type of the object,
    • a type that is the signed or unsigned type corresponding to the effective type of the object,
    • a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
    • an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or
    • a character type.

    In your example, *f = 1.0; is modifying fi.i, but the types are not compatible.

    I think the mistake is in thinking that a union contains n objects, where n is the number of members. A union contains only one active object at any point during program execution by §6.7.2.1 ¶16

    The value of at most one of the members can be stored in a union object at any time.

    Support for this interpretation that a union does not simultaneously contain all of its member objects can be found in §6.5.2.3:

    and if the union object currently contains one of these structures

    Finally, an almost identical issue was raised in defect report 236 in 2006.

    Example 2

    // optimization opportunities if "qi" does not alias "qd"
    void f(int *qi, double *qd) {
        int i = *qi + 2;
        *qd = 3.1;       // hoist this assignment to top of function???
        *qd *= i;
        return;
    }  
    
    main() {
        union tag {
            int mi;
            double md;
        } u;
        u.mi = 7;
        f(&u.mi, &u.md);
    }
    

    Committee believes that Example 2 violates the aliasing rules in 6.5 paragraph 7:

    "an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union)."

    In order to not violate the rules, function f in example should be written as:

    union tag {
        int mi;
        double md;
    } u;
    
    void f(int *qi, double *qd) {
        int i = *qi + 2;
        u.md = 3.1;   // union type must be used when changing effective type
        *qd *= i;
        return;
    }
    
    0 讨论(0)
  • 2020-12-29 04:10

    Let's back away from the standard for a second, and think about what's actually possible for a compiler.

    Suppose that strict_aliasing_example() is defined in strict_aliasing_example.c, and breaking_example() is defined in breaking_example.c. Assume both of these files are compiled separately and then linked together, like so:

    gcc -c -o strict_aliasing_example.o strict_aliasing_example.c
    gcc -c -o breaking_example.o breaking_example.c
    gcc -o breaking_example strict_aliasing_example.o breaking_example.o
    

    Of course we'll have to add a function prototype to breaking_example.c, which looks like this:

    int strict_aliasing_example(int *i, float *f);

    Now consider that the first two invocations of gcc are completely independent and cannot share information except for the function prototype. It is impossible for the compiler to know that i and j will point to members of the same union when it generates code for strict_aliasing_example(). There's nothing in the linkage or type system to specify that these pointers are somehow special because they came from a union.

    This supports the conclusion that other answers have mentioned: from the standard's point of view, accessing a union via . or -> obeys different aliasing rules compared with dereferencing an arbitrary pointer.

    0 讨论(0)
  • 2020-12-29 04:10

    Here is note 95 and its context:

    A postfix expression followed by the . operator and an identifier designates a member of a structure or union object. The value is that of the named member, (95) and is an lvalue if the first expression is an lvalue. If the first expression has qualified type, the result has the so-qualified version of the type of the designated member.

    (95) If the member used to read the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called “type punning”). This might be a trap representation.

    Note 95 clearly applies to an access via a union member. Your code does not do that. Two overlapping objects are accessed via pointers to 2 separate types, none of which is a character type, and none of which is a postfix expression pertinent for type punning.

    This is not a definitive answer...

    0 讨论(0)
提交回复
热议问题