C别名、强别名和restrict

如果两个指针引用同一内存地址,我们称一个指针是另一个指针的别名。别名并不罕见,不过可能会引发一些问题。下面的代码声明了两个指针,并把它们都指向同一地址:

int num = 5;
int* p1 = #
int* p2 = #

当编译器为指针生成代码时,除非特别指定,它必须假设可能会存在别名。使用别名会对编译器生成代码有所限制,如果两个指针引用同一位置,那么任何一个都可能修改这个位置。当编译器生成读写这个位置的代码时,它就不能通过把值放入寄存器来优化性能。对于每次引用,它只能执行机器级别的加载和保存操作。频繁的加载/保存会很低效,在某些情况下,编译器还必须关心操作执行的顺序。

强别名是另一种别名,它不允许一种类型的指针成为另一种类型的指针的别名。下面的代码中,一个整数指针是一个浮点数指针的别名,这破坏了强别名的规则。这段代码判断一个数是否为负数,相比将它的参数跟0比较来判断正负,这种方法执行速度更快:

float number = 3.25f;
unsigned int *ptrValue = (unsigned int *)&number;
unsigned int result = (*ptrValue & 0x80000000) == 0;

注意 强别名规则对符号或修饰符不起作用,下面都是合法的强别名:

int num;
const int *ptr1 = #
int *ptr2 = #
int volatile ptr3 = #

不过,有些情况下,对同样的数据采用不同的表现形式也是有用的,为了避免别名问题,可以采用这几种技术:

  • 使用联合体;
  • 关闭强别名;
  • 使用char指针。

两种数据类型的联合体可以避开强别名的问题,后面中会讨论这个主题。如果编译器有禁用强别名的选项,就可以关闭它。GCC编译器有如下的编译器选项:

  • -fno-strict-aliasing可以关闭强别名;
  • -fstrict-aliasing可以打开强别名;
  • -Wstrict-aliasing可以打开跟强别名相关的警告信息。

需要关闭强别名的代码可能意味着差劲的内存访问实践,如果可能的话,花些时间解决这些问题,而不是关闭强别名。

注意 编译器并非总能准确地报告别名相关的警告,有时候会漏报,有时候会虚报,最终还是要靠程序员定位别名问题。

编译器总是假定char指针是任意对象的潜在别名,所以,大部分情况下可以安全地使用。不过,把其他数据类型的指针转换成char指针,再把char指针转换成其他数据类型的指针,则会导致未定义的行为,应该避免这么做。

用联合体以多种方式表示值

C是类型语言,在声明变量时就得为其指定类型。可以存在不同类型的多个变量,有时候,可能需要把一种类型转换成另一种类型,这一般是通过类型转换实现的,不过也可以使用联合体。类型双关就是指这种绕开类型系统的技术。

如果转换涉及指针,可能会产生严重问题。为了说明这种技术,我们会用到三个不同的函数,这些函数会判断一个浮点数是否为正。

第一个函数用了浮点数和无符号整数的联合体,如下所示,函数先把浮点数赋给联合体,然后再获取整数来执行测试:

typedef union _conversion {
    float fNum;
    unsigned int uiNum;
} Conversion;

int isPositive1(float number) {
    Conversion conversion = { .fNum =number};
    return (conversion.uiNum & 0x80000000) == 0;
}

这样可以正确工作,也不会涉及别名,因为没有用到指针。下面这个版本用了两种数据类型的指针的联合体,将浮点数的地址赋给第一个指针,然后解引整数指针来执行测试。这样破坏了强别名规则:

typedef union _conversion2 {
    float *fNum;
    unsigned int *uiNum;
} Conversion2;

int isPositive2(float number) {
    Conversion2 conversion;
    conversion.fNum =&number;
    return (*conversion.uiNum & 0x80000000) == 0;
}

下面这个函数没有用联合体,而且破坏了强别名规则,因为ptrValue指针和number共享了同一个地址:

int isPositive3(float number) {
    unsigned int *ptrValue = (unsigned int *)&number;
    return (*ptrValue & 0x80000000) == 0;
}

这三个函数所用的方法做了如下假设:

  • 表示浮点数用的是IEEE-754浮点数标准;
  • 以特定方式布局浮点数;
  • 正确对齐了整数和浮点数指针。

不过,这些假设不一定有效。类似方法可以优化性能,但不一定可移植。如果移植性变得重要,执行浮点数比较是更好的办法。

强别名

编译器不会强制使用强别名,它只会产生警告。编译器假设两个或更多不同类型的指针永远不会引用同一个对象,这也包括除名字外其他都相同的结构体的指针。有了强别名,编译器可以做某些类型的优化。如果这个假设不正确,就会产生不可预期的结果。

即使两个结构体的字段完全一样,但如果名字不同的话,这两种结构体的指针就不应该引用同一对象。在下面的例子中,我们假设personemployee指针永远不会引用同一对象:

typedef struct _person {
    char* firstName;
    char* lastName;
    unsigned int age;
} Person;

typedef struct _employee {
    char* firstName;
    char* lastName;
    unsigned int age;
} Employee;

Person* person;
Employee* employee;

不过,如果定义了同一个结构体的两个类型,那么指向不同名字的指针可以引用同一对象:

typedef struct _person {
    char* firstName;
    char* lastName;
    unsigned int age;
} Person;

typedef Person Employee;

Person* person;
Employee* employee; 

使用restrict关键字

C编译器默认假设指针有别名,用restrict关键字可以在声明指针时告诉编译器这个指针没有别名,这样就允许编译器产生更高效的代码。很多情况下这是通过缓存指针实现的,不过要记住这只是个建议,编译器也可以选择不优化代码。如果用了别名,那么执行代码会导致未定义行为,编译器不会因为破坏强别名假设而提供任何警告信息。

注意 新开发的代码应该尽量对指针声明使用restrict关键字,这样会产生更高效的代码,而修改已有代码可能就不划算了。

下面这个函数说明restrict关键字的定义和使用,该函数把两个向量相加,并将结果存在第一个向量中:

void add(int size, double * restrict arr1, const double * restrict arr2) {
    for (int i = 0; i < size; i++) {
        arr1[i] += arr2[i];
    }
}

两个指针参数都用了restrict关键字,但是它们不应该引用同一块内存,下面是函数的正确用法:

double vector1[] = {1.1, 2.2, 3.3, 4.4};
double vector2[] = {1.1, 2.2, 3.3, 4.4};

add(4,vector1,vector2);

在下面的代码中,两个参数用了同一个向量,这样的调用是不正确的。第一个调用语句用了别名,而第二个调用语句则使用了同一个向量两次:

double vector1[] = {1.1, 2.2, 3.3, 4.4};
double *vector3 = vector1;

add(4,vector1,vector3);
add(4,vector1,vector1);

尽管这么做有时候能正确工作,但是调用函数的结果是不可靠的。

一些标准C函数用了restrict关键字,包括:

  • void *memcpy(void * restrict s1, const void * restrict s2, size_t n);
  • char *strcpy(char * restrict s1, const char * restrict s2);
  • char *strncpy(char * restrict s1, const char * restrict s2, size_t n);
  • int printf(const char * restrict format, ... );
  • int sprintf(char * restrict s, const char * restrict format, ... );
  • int snprintf(char * restrict s, size_t n, const char * restrict format, ... );
  • int scanf(const char * restrict format, ...);

restrict关键字隐含了两层含义:

  1. 对于编译器来说,这意味着它可以执行某些代码优化;
  2. 对于程序员来说,这意味着这些指针不能有别名,否则操作的结果将是未定义的。
赞(0)

评论 抢沙发

评论前必须登录!

 

C指针