restrict keyword in C (or restrict type qualifier in C)

restrict keyword in C (or restrict type qualifier in C)-min

In this blog post, you will learn the concept of the restrict keyword in C programming. You will also learn how to use the restrict Qualifiers with identifiers and their effect on them. But before describing the restrict qualifier want to discuss the C-type qualifiers. For better understanding, we will also see some programming examples of “restrict keyword”.

 

Type qualifier is a keyword that is applied to a type, resulting in a qualified type. Some C qualifiers are const, restrict (C99), volatile, and _Atomic (C11).

The const keyword is compiler-enforced and says that the program could not change the value of the object that means it makes the object a nonmodifiable type. For example, const int data is a qualified type representing a constant integer, where const is a type qualifier and we can not modify the value of “data”.

 

What is restrict qualifier (or restrict keyword)?

The restrict type qualifier, introduced in C99 and it is a special type qualifier and can be applied to pointer declarations. It qualifies the pointer, not what it points at. An object that is accessed through a restrict-qualified pointer has a special association with that pointer.

Basically, restrict is an optimization hint to the compiler that no other pointer in the current scope refers to the same memory location. That is, only the pointer or a value derived from it such as pointer + 1 is used to access the object during the lifetime of the pointer. This helps the compiler produce more optimized code. See the below programming examples for a better understanding.

 

Programming Examples using the restrict keyword:

Let’s see an example to understand how to restrict keywords optimize the code. Let consider the below function. The configuration which I am using, compiler x86-64 gcc (trunk)  with settings -std=c17 -O3.

Case 1: function with restrict keyword:

void copyArray(int n, int * restrict p, int * restrict q)
{
    while (n-- > 0)
    {
        *p++ = *q++;
    }
}

 

The compiler generates below assembly code:

copyArray:
        movslq  %edi, %rax
        movq    %rsi, %rdi
        movq    %rdx, %rsi
        testl   %eax, %eax
        jle     .L1
        leaq    0(,%rax,4), %rdx
        jmp     memcpy
.L1:
        ret

 

 

Case 2: function without restrict keyword:

Now remove the restrict keyword from the function and check the assembly code generated by the compiler with the same configuration.

void copyArray(int n, int *p, int *q)
{
    while (n-- > 0)
    {
        *p++ = *q++;
    }
}

 

The compiler generates below assembly code without the restrict keyword. You can see that code is less optimized.

copyArray:
        movl    %edi, %r8d
        movq    %rsi, %rcx
        leal    -1(%rdi), %edi
        testl   %r8d, %r8d
        jle     .L1
        leaq    4(%rdx), %rsi
        movq    %rcx, %rax
        subq    %rsi, %rax
        cmpq    $8, %rax
        jbe     .L3
        cmpl    $2, %edi
        jbe     .L3
        movl    %r8d, %esi
        xorl    %eax, %eax
        shrl    $2, %esi
        salq    $4, %rsi
.L4:
        movdqu  (%rdx,%rax), %xmm0
        movups  %xmm0, (%rcx,%rax)
        addq    $16, %rax
        cmpq    %rsi, %rax
        jne     .L4
        movl    %r8d, %esi
        andl    $-4, %esi
        movl    %esi, %eax
        subl    %esi, %edi
        salq    $2, %rax
        addq    %rax, %rcx
        addq    %rdx, %rax
        andl    $3, %r8d
        je      .L1
        movl    (%rax), %edx
        movl    %edx, (%rcx)
        testl   %edi, %edi
        jle     .L1
        movl    4(%rax), %edx
        movl    %edx, 4(%rcx)
        cmpl    $1, %edi
        jle     .L1
        movl    8(%rax), %eax
        movl    %eax, 8(%rcx)
        ret
.L3:
        movslq  %r8d, %rsi
        xorl    %eax, %eax
.L6:
        movl    (%rdx,%rax,4), %edi
        movl    %edi, (%rcx,%rax,4)
        addq    $1, %rax
        cmpq    %rsi, %rax
        jne     .L6
.L1:
        ret

 

Note: A translator is free to ignore any or all aliasing implications of uses of restrict.

 

 

Some important points related to the restrict qualifier:

 

1. The intended use of the restrict qualifier (like the register storage class) is to promote optimization.

2.  If a pointer p qualify with restrict,  it tells the compiler that pointer p is the only way to access the object pointed by it in the current scope.

int data = 12;
{
    int* restrict p1 = &data;
    
    int*  p2 = &data; // undefined behavior
}

 

3. Assignment from one restricted pointer to another is undefined behavior within the same scope.

void fun()
{
    int* restrict p1 = &data1;
    
    int* restrict p2 = &data2;
    
    p1 = p2; // undefined behavior
    
    /*Doing some work*/
}

 

4. The benefit of the restrict qualifiers is that they enable a translator to make an effective dependence analysis of function without examining any of the calls of the function in the program. But it is the responsibility of the programmer to examine all of those calls to ensure that none give undefined behavior.

See the below example, the second call of fun() in test() has undefined behavior because each of arr[1] through arr[49] is accessed through both p and q.

void fun(int n, int * restrict p, int * restrict q)
{
    while(n-- > 0)
    {
        *p++ = *q++; 
    }
}


void test(void)
{
    extern int arr[100];

    fun(50, arr + 50, arr); // Valid

    fun(50, arr + 1, arr);  /* Undefined behavior:
                       arr[1] to arr[49] is accessed through both p and q in fun()
                       */
}

 

So if you are using the restrict in your function parameters you must examine each call of the function. Because it might give undefined behavior with improper inputs.

 

5. You can alias an unmodified object through two restricted pointers. See the below example.

void fun(int n, int * restrict p, int * restrict q, int * restrict r)
{
    int i;
    for (i = 0; i < n; i++)
    {
        p[i] = q[i] + r[i];
    }
}

 

In particular, if a and b are disjoint arrays, a call of the form fun(100, a, b, b) has defined behavior, because array “b” is not modified within function fun().

 

6. A translator is free to ignore any or all aliasing implications of uses of restrict. It means the compiler is free to ignore the restrict keyword.

 

7. We know that assignment from one restricted pointer to another is undefined behavior within the same scope. But here is one exception, ‘‘outer-to-inner’’ assignments between restricted pointers declared in nested blocks have defined behavior.

See the below example where we are declaring a new restricted pointer based on the original one. So there is no UB (undefined behavior).

{
    int * restrict p1;
    int * restrict q1;
    p1 = q1; // undefined behavior
    
    {
        int * restrict p2 = p1; // valid
        int * restrict q2 = q1; // valid
        
        p1 = q2; // undefined behavior
        p2 = q2; // undefined behavior
    }
}

 

8. Restricted pointers can be assigned to unrestricted pointers freely.

For example,

void fun(int n, float * restrict r, float * restrict s)
{
    float * p = r, * q = s; // OK
    while(n-- > 0)
    {
        *p++ = *q++;
    }
}

The lvalue *p that is used to access the underlying array pointed by r has its address based on r. In another word you can say that *pis indirect access of underlying array through r.  It is similar for *q. Because all the accesses occur, even if indirectly, through the originally restricted pointers. So no undefined behavior.

 

Recommended Post

Leave a Reply

Your email address will not be published. Required fields are marked *