1

我有一个例程,它使用适当大小的数组来处理额外的字节,将精度数据数组提升为双精度:

void dpromote(const int n, double *x)             
{
    for (int i = n; i --> 0 ;) {    
        x[i] = ((float *)x)[i];
    }
}

进入x时应包含n floats,退出时应包含n doubles:

void test_dpromote()
{
    double  e[]   = {1, 2, 3, 4, 5, 6, 7};
    float   x[]   = {1, 2, 3, 4, 5, 6, 7, 0, 0, 0, 0, 0, 0, 0};
    const int n = sizeof(e)/sizeof(e[0]);
    dpromote(n, (void *) x);
    /* memcmp(x, e, sizeof(e)) will return 0 when this works as expected */
}

我为什么要这样做?数字密集型代码中的混合精度迭代细化。出于这个问题的目的,您可以忽略原因,因为它确实无关紧要。

多个编译器可以dpromote在启用严格别名的各种积极优化级别上处理逻辑。最近,一位编译器供应商(将保持匿名)决定重新索引我的循环,以便它是通过内存的前向遍历而不是向后遍历。如果你盯着代码半分钟,你会发现循环转换产生了完全的垃圾。

dpromote启用所有 C99 严格混叠花哨的逻辑是否依赖于未定义的行为?我不明白为什么编译器会认为可以更改循环索引,除非代码正在做未定义的事情。

4

2 回答 2

0

是的,您违反了严格的别名规则。使用联合 - 它可能不会保持您的原始布局,但它可以更好地反映您的意图并且通常更清洁:

#include <stdio.h>

union value 
{
    double d;
    float f;
};

void dpromote (const int n, value* x)
{
    for (int i=0; i < n; ++i)
        x[i].d = x[i].f;
}

void test_dpromote()
{
    value x[] = {{.f=1}, {.f=2}, {.f=3}, {.f=4}, {.f=5}, {.f=6}, {.f=7}};
    const int n = sizeof(x) / sizeof(x[0]);

    for (int i=0; i < n; ++i)
        printf("float: %f\n", x[i].f);

    dpromote(n, x);

    for (int i=0; i < n; ++i)
        printf("double: %f\n", x[i].d);
}

int main ()
{
    test_dpromote();
    return 0;
}

如果您必须保持原始布局,则需要手动管理内存块以满足严格的别名规则:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

double* dpromote (const int n, char* x)
{
    for (int i=n; i-- > 0; )
    {
        float f;
        memcpy(&f, &x[i*sizeof(float)], sizeof(f));
        double d = f;
        memcpy(&x[i*sizeof(double)], &d, sizeof(d));
    }

    return (double*)x;
}

void test_dpromote()
{
    int const n = 7;
    char* block = (char*)malloc(n*sizeof(double));
    for (int i=0; i < n; ++i)
    {
        float const x = i+1;
        memcpy(&block[i*sizeof(float)], &x, sizeof(x));
    }

    // It is now safe to access block through x

    float* x = (float*)block;
    for (int i=0; i < n; ++i)
        printf("float: %f\n", x[i]);

    double* y = dpromote(n, block);
    for (int i=0; i < n; ++i)
        printf("double: %f\n", y[i]);

    // It is now safe to access block through y, however
    // subsequent access through x will violate strict aliasing rules
}

int main ()
{
    test_dpromote();
    return 0;
}
于 2013-02-21T06:57:32.943 回答
0

该标准要求编译器在一些不太可能有用的地方做出悲观的别名假设,但被解释为邀请编译器忽略其他类型别名的证据。我认为如果读取为 float 的每个元素之前都被写为 float,并且在任何将其写为“double”的操作之前排序,那么您的代码将定义行为。但是,我认为您的代码在最后一次迭代中调用了 UB,因为分配读取和写入重叠的对象。

如果你的代码写成:

void dpromote(const int n, double *x)             
{
    float *fp = x;
    for (int i = n; i --> 0 ;) {    
        double d = fp[i];
        x[i] = d;
    }
}

我不会特别谴责没有注意到混叠的编译器,因为循环内没有证据表明可能会发生混叠。我不热衷于编译器重新安排循环的行为,尽管考虑到重叠的读写,我不会指责由于其他原因而出现故障的编译器[例如,如果针对没有 FPU 的 CPU 的编译器做了类似 clear some “双”的位,然后用原始指数和尾数的位移版本覆盖它的部分]。但是,如果将分配拆分为单独的读取和写入并不能修复编译器的行为,那么我会认为它是狡猾的,因为这种行为既不符合标准也不符合常识。

于 2017-10-05T20:56:51.277 回答