I encountered the following question when using vector intrinsics (AVX), but the question probably also applies to sequential programming. It concerns the use of the restrict keyword. The keyword is available in C99, but not in C++ (except for special keywords provided by most compilers). My question is: Can I avoid using restrict by copying data from non-restrict pointer arguments to local variables? It works in my example, but is this behavior guaranteed?
Here's my code with 4 different versions of SIMD vector addition. The first version vecAdd1() passes the arguments as restrict pointers. All other versions use normal (non-restrict) pointers as arguments. The second version vecAdd2() has no further code modifications. The third version vecAdd3() copies the data pointer of each struct into a local variable. The fourth version vecAdd4() also does the same for the size n.
#include <stdio.h>
#include <x86intrin.h>
#define N 8 // 8 floats per AVX vector
#define SIZE 1000 // 1000 floats per data vector
typedef struct { int n; float *data; } Vec;
void vecCreate(int size, Vec *v) {
v->n = size;
posix_memalign((void**)&(v->data), 32, size * sizeof(float));
}
void vecAdd1(Vec * restrict a, Vec * restrict b, Vec * restrict c) {
__m256 va, vb, vc;
for (int i = 0; i <= (a->n - N); i += N) {
va = _mm256_load_ps(a->data + i);
vb = _mm256_load_ps(b->data + i);
vc = _mm256_add_ps(va, vb);
_mm256_store_ps(c->data + i, vc);
}
}
void vecAdd2(Vec *a, Vec *b, Vec *c) {
__m256 va, vb, vc;
for (int i = 0; i <= (a->n - N); i += N) {
va = _mm256_load_ps(a->data + i);
vb = _mm256_load_ps(b->data + i);
vc = _mm256_add_ps(va, vb);
_mm256_store_ps(c->data + i, vc);
}
}
void vecAdd3(Vec *a, Vec *b, Vec *c) {
__m256 va, vb, vc;
float *pa = a->data, *pb = b->data, *pc = c->data;
for (int i = 0; i <= (a->n - N); i += N) {
va = _mm256_load_ps(pa + i);
vb = _mm256_load_ps(pb + i);
vc = _mm256_add_ps(va, vb);
_mm256_store_ps(pc + i, vc);
}
}
void vecAdd4(Vec *a, Vec *b, Vec *c) {
__m256 va, vb, vc;
float *pa = a->data, *pb = b->data, *pc = c->data;
int ae = a->n - N;
for (int i = 0; i <= ae; i += N) {
va = _mm256_load_ps(pa + i);
vb = _mm256_load_ps(pb + i);
vc = _mm256_add_ps(va, vb);
_mm256_store_ps(pc + i, vc);
}
}
int
main()
{
Vec a, b, c;
vecCreate(1000, &a);
vecCreate(1000, &b);
vecCreate(1000, &c);
vecAdd1(&a, &b, &c);
vecAdd2(&a, &b, &c);
vecAdd3(&a, &b, &c);
vecAdd4(&a, &b, &c);
printf("%g\n", c.data[123]);
return 0;
}
(Just a comment: The -N and <= is used to limit processing to the part where entire SIMD vectors can be loaded and stored. I omitted the sequential postamble.)
Here's the compiler invocation:
gcc -O3 -mno-avx256-split-unaligned-load -mno-avx256-split-unaligned-store -march=native -masm=intel -save-temps -std=c99 -Wall -o vecadd vecadd.c
I'm using version 7.5.0. In the following I only show the relevant portions of the assembly code from vecadd.s.
In vecAdd1(), the loop has a very efficient implementation: load one SIMD vector, add the second, store to result, advance pointer, check for loop end:
.L5:
vmovaps ymm0, YMMWORD PTR [rdi+rax]
vaddps ymm0, ymm0, YMMWORD PTR [rsi+rax]
vmovaps YMMWORD PTR [rcx+rax], ymm0
add rax, 32
cmp rdx, rax
jne .L5
If I leave out the restrict keyword in the argument list in vecAdd2(), the loop gets very inefficient: Within the loop, the three data pointers and the size n are reloaded every time, before the SIMD vectors are loaded, processed, and stored, and the loop condition is checked:
.L10:
mov r10, QWORD PTR 8[rdi]
mov r9, QWORD PTR 8[rsi]
add r8d, 8
mov rcx, QWORD PTR 8[rdx]
vmovaps ymm0, YMMWORD PTR [r10+rax]
vaddps ymm0, ymm0, YMMWORD PTR [r9+rax]
vmovaps YMMWORD PTR [rcx+rax], ymm0
mov ecx, DWORD PTR [rdi]
add rax, 32
sub ecx, 7
cmp ecx, r8d
jg .L10
In version vecAdd3(), the data pointers are not reloaded (they are loaded once before the loop), but the size n is reloaded:
.L15:
vmovaps ymm0, YMMWORD PTR -32[r8+rax*4]
mov ecx, eax
vaddps ymm0, ymm0, YMMWORD PTR -32[rsi+rax*4]
vmovaps YMMWORD PTR -32[r9+rax*4], ymm0
mov edx, DWORD PTR [rdi]
add rax, 8
sub edx, 7
cmp edx, ecx
jg .L15
Only if I copy all data pointers and n to local variables in vecAdd4(), the code looks like the one in vecAdd1():
.L20:
vmovaps ymm0, YMMWORD PTR [rcx+rax]
vaddps ymm0, ymm0, YMMWORD PTR [rsi+rax]
vmovaps YMMWORD PTR [r8+rax], ymm0
add rax, 32
cmp rdx, rax
jne .L20
So, to repeat my question: Assume I want to avoid the compiler-specific restrict replacements in C++. I therefore use non-restrict arguments, but copy them to local (also non-restrict) variables. Is it guaranteed that the compiler doesn't assume for the local variables that they can alias (even if the function arguments can)?
(Side question: Is it relevant for this question that I copy struct components?)