严格的混叠和对齐 [英] strict aliasing and alignment

查看:148
本文介绍了严格的混叠和对齐的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我需要一个安全的方式来任意POD类型之间的别名,符合ISO-C ++ 11明确考虑3.10 / 10和3.11的n3242或更高版本。
这里有很多关于严格别名的问题,其中大部分是关于C而不是C ++的。我发现一个使用联合的C的解决方案,可能使用这个


联合类型,其中包括上述类型之一b $ b元素或非静态数据成员


我从中构建了这个。

  #include< iostream> 

template< typename T,typename U>
T& access_as(U * p)
{
union dummy_union
{
U dummy;
T destination;
};

dummy_union * u =(dummy_union *)p;

return u-> destination;
}

struct test
{
short s;
int i;
};

int main()
{
int buf [2];

static_assert(sizeof(buf)> = sizeof(double),);
static_assert(sizeof(buf)> = sizeof(test),);

access_as< double>(buf)= 42.1337;
std :: cout<< access_as< double>(buf)<< '\\\
';

access_as< test>(buf).s = 42;
access_as< test>(buf).i = 1234;

std :: cout<< access_as< test>(buf).s< '\\\
';
std :: cout<< access_as< test>(buf).i< '\\\
';
}

我的问题是,只是为了确保,标准? *



在使用MinGW / GCC 4.6.2编译时,它不会发出任何警告, >

  g ++ -std = c ++ 0x -Wall -Wextra -O3 -fstrict-aliasing -o alias.exe alias.cpp 

* 编辑:如果没有,怎么修改这个是合法的? / p>

解决方案

这不会是合法的,无论你用奇怪的演员和工会以及什么来执行什么样的扭曲。

基本的事实是这样的:不同类型的两个对象可能永远不会在内存中别名,有一些特殊的例外(见下文)。



示例



请考虑以下代码:

  sum(double& out,float * in,int count){
for(int i = 0; i out + = * in ++;
}
}

让我们将其分解为局部寄存器变量执行更紧密:

  void sum(double& out,float * in,int count){
for i = 0; i register double out_val = out; //(1)
register double in_val = * in; //(2)
register double tmp = out_val + in_val;
out = tmp; / /(3)
in ++;
}
}

假设(1),(2)和3)分别表示存储器读,读和写,这在这种紧密的内部循环中可能是非常昂贵的操作。此循环的合理优化如下:

  void sum(double& out,float * in,int count){ 
register double tmp = out; //(1)
for(int i = 0; i register double in_val = * in; //(2)
tmp = tmp + in_val;
in ++;
}
out = tmp; //(3)
}

此优化会将所需的内存读取次数减少一半内存写入次数为1.这对代码的性能有很大的影响,对所有优化C和C ++编译器都是非常重要的优化。



现在,假设我们没有严格的别名。假设对任何类型的对象的写入可以影响任何其他对象。假设写入double可以影响某个float的值。这使得上述优化是可能的,因为程序员实际上可能意图用于out和in别名,使得sum函数的结果更复杂并且受到进程的影响。听起来很蠢?即使如此,编译器也无法区分愚蠢和智能代码。编译器只能区分格式良好和不合格的代码。如果我们允许自由别名,那么编译器必须在其优化中保守,并且必须在循环的每次迭代中执行额外存储(3)。



希望你能看到现在为什么没有这样的联盟或铸造技巧可能是合法的。



严格别名的异常



C和C ++标准规定了对 char 以及任何相关类型(包括派生和基本类型)和成员进行别名任何类型的特殊规定,因为能够使用地址的类成员独立是如此重要。您可以在此答案中找到这些条款的详尽列表。



<此外,GCC还特别规定从联盟的不同成员阅读,而不是上次写的。注意,这种通过union的转换实际上不允许你违反别名。只有一个联盟的成员被允许在任何一个时间是活动的,例如,即使使用GCC,以下是未定义的行为:

  union {
double d;
float f [2];
};
f [0] = 3.0f;
f [1] = 5.0f;
sum(d,f,2); // UB:尝试处理
的两个成员//同时处于活动状态的联合



解决方法



将一个对象的位重新解释为其他类型的对象的唯一标准方法是使用 memcpy 。这利用了对 char 对象进行别名的特殊规定,实际上允许在字节级读取和修改底层对象表示。例如,以下是合法的,并且不违反严格别名规则:

  int a [2] 
double d;
static_assert(sizeof(a)== sizeof(d));
memcpy(a,& d,sizeof(d));

这在语义上等同于以下代码:

  int a [2]; 
double d;
static_assert(sizeof(a)== sizeof(d));
for(size_t i = 0; i ((char *)a)[i] =((char *)& d)[i] ;

GCC提供从非活动联盟成员读取的设置,从 GCC文档:


从与最近写入的成员不同的成员(称为类型冲击)读取的做法很常见。即使使用-fstrict别名,只要通过联合类型访问内存,就允许类型划分。因此,上面的代码将按预期工作。请参见结构联合枚举和位字段实现。但是,此代码可能不会:




  int f(){
union a_union t;
int * ip;
t.d = 3.0;
ip =& t.i;
return * ip;
}




同样,生成的指针和取消引用结果具有未定义的行为,即使转换使用联合类型,例如:




  int f(){
double d = 3.0;
return((union a_union *)& d) - > i;
}



新放置



(注意:我在这里记忆,因为我现在没有访问标准)。
一旦放置 - 将对象新建到存储缓冲区中,底层存储对象的生命周期就会隐式结束。这类似于写给联盟成员时发生的情况:

  union {
int i;
float f;
} u;

// u的成员没有活动。 i和f都不指任何类型的左值。
u.i = 5;
//成员u.i现在是活动的,并且存在值为5的类型为int的lvalue(object)
//。不存在float对象。
u.f = 5.0f;
//成员u.i不再活动,
//因为它的生命周期已经结束了赋值。
//成员u.f现在是活动的,并且存在值为5.0f的类型为float的lvalue(object)
//。不存在int对象。

现在,让我们看看与placement-new类似的:

  #define MAX_(x,y)((x)>(y)?(x):(y))
//对齐的内存
char * buffer = new char [MAX_(sizeof(int),sizeof(float))];
//目前,只有char对象存在于缓冲区中。
new(buffer)int(5);
//在缓冲区指向的内存中构造了一个int类型的对象,
//隐含地终止底层存储对象的生命周期。
new(buffer)float(5.0f);
//缓冲区指向的内存中构造了一个int类型的对象,
//隐含地结束以前占用相同内存的int对象的生命周期。

这种隐式的终止时间只能用于具有琐碎的构造函数和析构函数的类型,明显的原因。


I need a safe way to alias between arbitrary POD types, conforming to ISO-C++11 explicitly considering 3.10/10 and 3.11 of n3242 or later. There are a lot of questions about strict aliasing here, most of them regarding C and not C++. I found a "solution" for C which uses unions, probably using this section

union type that includes one of the aforementioned types among its elements or nonstatic data members

From that I built this.

#include <iostream>

template <typename T, typename U>
T& access_as(U* p)
{
    union dummy_union
    {
        U dummy;
        T destination;
    };

    dummy_union* u = (dummy_union*)p;

    return u->destination;
}

struct test
{
    short s;
    int i;
};

int main()
{
    int buf[2];

    static_assert(sizeof(buf) >= sizeof(double), "");
    static_assert(sizeof(buf) >= sizeof(test), "");

    access_as<double>(buf) = 42.1337;
    std::cout << access_as<double>(buf) << '\n';

    access_as<test>(buf).s = 42;
    access_as<test>(buf).i = 1234;

    std::cout << access_as<test>(buf).s << '\n';
    std::cout << access_as<test>(buf).i << '\n';
}

My question is, just to be sure, is this program legal according to the standard?*

It doesn't give any warnings whatsoever and works fine when compiling with MinGW/GCC 4.6.2 using:

g++ -std=c++0x -Wall -Wextra -O3 -fstrict-aliasing -o alias.exe alias.cpp

* Edit: And if not, how could one modify this to be legal?

解决方案

This will never be legal, no matter what kind of contortions you perform with weird casts and unions and whatnot.

The fundamental fact is this: two objects of different type may never alias in memory, with a few special exceptions (see further down).

Example

Consider the following code:

void sum(double& out, float* in, int count) {
    for(int i = 0; i < count; ++i) {
        out += *in++;
    }
}

Let's break that out into local register variables to model actual execution more closely:

void sum(double& out, float* in, int count) {
    for(int i = 0; i < count; ++i) {
        register double out_val = out; // (1)
        register double in_val = *in; // (2)
        register double tmp = out_val + in_val;
        out = tmp; // (3)
        in++;
    }
}

Suppose that (1), (2) and (3) represent a memory read, read and write, respectively, which can be very expensive operations in such a tight inner loop. A reasonable optimization for this loop would be the following:

void sum(double& out, float* in, int count) {
    register double tmp = out; // (1)
    for(int i = 0; i < count; ++i) {
        register double in_val = *in; // (2)
        tmp = tmp + in_val;
        in++;
    }
    out = tmp; // (3)
}

This optimization reduces the number of memory reads needed by half and the number of memory writes to 1. This can have a huge impact on the performance of the code and is a very important optimization for all optimizing C and C++ compilers.

Now, suppose that we don't have strict aliasing. Suppose that a write to an object of any type can affect any other object. Suppose that writing to a double can affect the value of a float somewhere. This makes the above optimization suspect, because it's possible the programmer has in fact intended for out and in to alias so that the sum function's result is more complicated and is affected by the process. Sounds stupid? Even so, the compiler cannot distinguish between "stupid" and "smart" code. The compiler can only distinguish between well-formed and ill-formed code. If we allow free aliasing, then the compiler must be conservative in its optimizations and must perform the extra store (3) in each iteration of the loop.

Hopefully you can see now why no such union or cast trick can possibly be legal. You cannot circumvent fundamental concepts like this by sleight of hand.

Exceptions to strict aliasing

The C and C++ standards make special provision for aliasing any type with char, and with any "related type" which among others includes derived and base types, and members, because being able to use the address of a class member independently is so important. You can find an exhaustive list of these provisions in this answer.

Furthermore, GCC makes special provision for reading from a different member of a union than what was last written to. Note that this kind of conversion-through-union does not in fact allow you to violate aliasing. Only one member of a union is allowed to be active at any one time, so for example, even with GCC the following would be undefined behavior:

union {
    double d;
    float f[2];
};
f[0] = 3.0f;
f[1] = 5.0f;
sum(d, f, 2); // UB: attempt to treat two members of
              // a union as simultaneously active

Workarounds

The only standard way to reinterpret the bits of one object as the bits of an object of some other type is to use an equivalent of memcpy. This makes use of the special provision for aliasing with char objects, in effect allowing you to read and modify the underlying object representation at the byte level. For example, the following is legal, and does not violate strict aliasing rules:

int a[2];
double d;
static_assert(sizeof(a) == sizeof(d));
memcpy(a, &d, sizeof(d));

This is semantically equivalent to the following code:

int a[2];
double d;
static_assert(sizeof(a) == sizeof(d));
for(size_t i = 0; i < sizeof(a); ++i)
   ((char*)a)[i] = ((char*)&d)[i];

GCC makes a provision for reading from an inactive union member, implicitly making it active. From the GCC documentation:

The practice of reading from a different union member than the one most recently written to (called "type-punning") is common. Even with -fstrict-aliasing, type-punning is allowed, provided the memory is accessed through the union type. So, the code above will work as expected. See Structures unions enumerations and bit-fields implementation. However, this code might not:

int f() {
    union a_union t;
    int* ip;
    t.d = 3.0;
    ip = &t.i;
    return *ip;
}

Similarly, access by taking the address, casting the resulting pointer and dereferencing the result has undefined behavior, even if the cast uses a union type, e.g.:

int f() {
    double d = 3.0;
    return ((union a_union *) &d)->i;
} 

Placement new

(Note: I'm going by memory here as I don't have access to the standard right now). Once you placement-new an object into a storage buffer, the lifetime of the underlying storage objects ends implicitly. This is similar to what happens when you write to a member of a union:

union {
    int i;
    float f;
} u;

// No member of u is active. Neither i nor f refer to an lvalue of any type.
u.i = 5;
// The member u.i is now active, and there exists an lvalue (object)
// of type int with the value 5. No float object exists.
u.f = 5.0f;
// The member u.i is no longer active,
// as its lifetime has ended with the assignment.
// The member u.f is now active, and there exists an lvalue (object)
// of type float with the value 5.0f. No int object exists.

Now, let's look at something similar with placement-new:

#define MAX_(x, y) ((x) > (y) ? (x) : (y))
// new returns suitably aligned memory
char* buffer = new char[MAX_(sizeof(int), sizeof(float))];
// Currently, only char objects exist in the buffer.
new (buffer) int(5);
// An object of type int has been constructed in the memory pointed to by buffer,
// implicitly ending the lifetime of the underlying storage objects.
new (buffer) float(5.0f);
// An object of type int has been constructed in the memory pointed to by buffer,
// implicitly ending the lifetime of the int object that previously occupied the same memory.

This kind of implicit end-of-lifetime can only occur for types with trivial constructors and destructors, for obvious reasons.

这篇关于严格的混叠和对齐的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆