Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 7

Pointer Aliasing and the `restrict` Keyword

Recommended resources

Hello! Welcome to the seventh lesson in our module on Advanced C Programming & System Memory.

In our previous lesson, we tackled critical memory safety bugs like dangling pointers, memory leaks, and double-free errors. We established that meticulous pointer management is essential for program stability. Now, we shift our focus from memory safety to a concept that is paramount for performance, especially in the high-performance computing and semiconductor domains: pointer aliasing.

This lesson is designed to address the learning outcome: Explain pointer aliasing problems and apply the restrict keyword to enable compiler optimizations in performance-critical code.

Understanding aliasing is a key differentiator in interviews for companies like AMD, Nvidia, and Qualcomm, where extracting maximum performance from the hardware is a primary concern. You'll be expected to understand not just how to write correct code, but how to write code that allows the compiler to generate the most optimal machine instructions.

We will cover:

  1. What pointer aliasing is and why it forces compilers to be overly cautious.
  2. How the C99 restrict keyword serves as a "promise" to the compiler.
  3. The performance gains restrict enables, especially vectorization.
  4. The dangers of breaking this promise and the subtle bugs it can create.
  5. How this concept applies to the classic interview question: memcpy vs. memmove.

1. The Problem: What is Pointer Aliasing?

In simple terms, aliasing occurs when two or more different pointers or expressions refer to the same memory location. The compiler can't always know if pointers are aliased, and this uncertainty has significant consequences for optimization.

Visualizing Pointer Aliasing
This image shows two scenarios. On the left, pointers A and B alias because the memory regions they point to overlap. On the right, they do not alias, pointing to distinct, non-overlapping memory regions.

Let's consider a simple function:

void scale_and_update(int* data, int* multiplier) {
    *data = *data * (*multiplier);
    // Some other work...
    *data = *data + (*multiplier);
}

What if data and multiplier point to the same memory location?

  • The first line would change the value of *multiplier as well.
  • The second line would then use this new value of *multiplier.

Because the compiler cannot rule out this possibility, it must generate conservative code. Specifically, it must reload the value of *multiplier from memory for the second operation, even if it already had the value in a register. This prevents optimizations.

To understand the performance cost of aliasing, let's watch a segment of a CppCon talk that explains the optimization opportunities a compiler must forgo when aliasing is a possibility.

Aliasing in C++ - Risks, Opportunities and Techniques - Roi Barkan - CppCon 2022

This video by Roi Barkan clearly explains the types of optimizations compilers are forced to skip due to potential aliasing. Focus on the three main missed opportunities he discusses.

Watch the segment from 18:04 to 22:57. Pay close attention to the explanation of how aliasing prevents: Register Allocation: Storing variables in fast CPU registers. Vectorization: Using SIMD instructions to perform multiple operations at once. Expression Hoisting: Moving calculations (like sin(coefficient)) out of a loop.

As the video showed, the compiler's inability to assume pointers are distinct leads to significant performance loss. It's forced to generate code that repeatedly accesses memory, cannot reorder instructions freely, and cannot use powerful parallel hardware features like vector units.


2. The Solution: The restrict Keyword

To solve this, C99 introduced the restrict keyword. It is a type qualifier for pointers, unique to the C language (it has no standard equivalent in C++).

restrict is a promise from you, the programmer, to the compiler. When you declare a pointer as restrict, you guarantee that for the lifetime of that pointer, the object it points to will only be accessed (read or written) through that specific pointer.

// With restrict, you promise the compiler that 'dst' and 'src' do not overlap.
void my_memcpy(void* restrict dst, const void* restrict src, size_t n);

Let's watch a video that introduces restrict and shows a simple optimization it enables.

The ONLY C keyword with no C++ equivalent

The mCoding channel provides an excellent, concise explanation of restrict. This first clip introduces the keyword and shows how it can eliminate a redundant memory read.

Watch from the beginning to 3:41. The video will first establish what restrict is and then demonstrate with a simple assembly comparison how it allows the compiler to avoid reloading a variable from memory.

Enabling Vectorization

The real power of restrict shines in loops performing numerical calculations, a common scenario in graphics, signal processing, and scientific computing. By promising that output and input arrays don't overlap, you allow the compiler to use SIMD (Single Instruction, Multiple Data) instructions, also known as vector instructions.

Instead of adding one pair of numbers at a time, the CPU can add 4, 8, or even more pairs in a single instruction cycle.

The ONLY C keyword with no C++ equivalent

Let's continue with the mCoding video to see a more impactful example involving vector addition. This is a classic use case for restrict.

Watch the segment from 3:41 to 6:33. Observe how adding restrict allows the compiler to generate a much simpler, vectorized loop, removing the complex runtime checks for pointer overlap.


3. Practical Applications and Dangers

memcpy vs. memmove: A Classic Interview Question

The difference between memcpy and memmove is a direct application of the restrict concept.

  • memcpy(void* restrict dst, const void* restrict src, size_t n);
    The function signature (and C standard) specifies that the source and destination pointers are restricted. This means the behavior is undefined if the memory regions overlap. Because of this guarantee, memcpy can be implemented as a highly optimized, simple copy loop.

  • memmove(void* dst, const void* src, size_t n);
    This function makes no such promise. It is guaranteed to work correctly even if the source and destination regions overlap. To achieve this, its implementation might be more complex, potentially checking for overlap and using a temporary buffer if necessary.

In an interview, you should be able to state this clearly: memcpy is faster but requires non-overlapping memory, a condition guaranteed by the restrict semantics. memmove is safer for overlapping memory but may be slower.

The ONLY C keyword with no C++ equivalent

The mCoding video also covers this exact distinction. It's a great summary.

Watch from 9:05 to 10:22 for a concise explanation of how memcpy and memmove relate to restrict.

The Danger: Breaking the Promise

restrict is a powerful tool, but it's also dangerous. If you lie to the compiler—that is, you use restrict but the pointers do alias—you invoke undefined behavior.

The resulting bugs are often insidious:

  • The code might work perfectly fine with optimizations turned off (-O0).
  • The bug may only appear in release builds with high optimization levels (-O2, -O3).
  • The symptoms can be bizarre and seemingly unrelated to the code that's actually at fault.

This is a nightmare debugging scenario and a great interview question to test your depth of knowledge.

The ONLY C keyword with no C++ equivalent

Finally, let's look at what happens when you misuse restrict. This is the most critical part to understand for safe and effective use.

Watch from 6:33 to 9:05. The Fibonacci example is perfect. Notice how the incorrect code produces the right answer at -O0 but the wrong answer at -O3. This is a classic sign of violating a compiler optimization rule like restrict.


4. A Related Concept: The Strict Aliasing Rule

While restrict is an explicit promise you make, C also has an implicit rule called the strict aliasing rule. This rule states that it is undefined behavior to access an object using a pointer of an incompatible type.

For example, if you have an int, you cannot legally access its memory through a float*.

Understanding Type-Based Alias Analysis in C and C++

To solidify our understanding of aliasing, let's briefly look at this type-based rule. This article provides a clear introduction and a practical example.

Read the sections "What are Type Systems?", "Let’s Talk about Type-based Alias Analysis", and the code example that follows, which shows different results at different optimization levels. This will give you a foundational understanding of how compilers use types to reason about aliasing.

Because of this rule, the compiler can assume that an int* and a float* will never point to the same object, allowing for optimizations similar to those enabled by restrict, but based on type information instead of an explicit keyword. There are exceptions to this rule (e.g., char* can alias anything), which we will explore in the next lesson.


Conclusion

You now have a solid understanding of one of the most advanced and performance-critical aspects of C programming. For an experienced engineer targeting semiconductor companies, this knowledge is not just academic—it's essential.

Key Takeaways:

  • Pointer Aliasing: Occurs when multiple pointers access the same memory, forcing compilers into conservative, slow code generation.
  • Performance Impact: Aliasing prevents key optimizations like register caching, instruction reordering, and crucially, vectorization (SIMD).
  • restrict Keyword: A C-specific promise to the compiler that a pointer provides exclusive access to an object, unlocking aggressive optimizations.
  • The Danger of restrict: Violating the restrict promise results in undefined behavior that often manifests only at high optimization levels, leading to subtle and hard-to-debug failures.
  • memcpy vs. memmove: memcpy assumes no overlap (as if pointers were restricted) and is fast. memmove handles overlap correctly and is safer but potentially slower.
  • Strict Aliasing Rule: A related, implicit rule that allows compilers to assume pointers of incompatible types do not alias.

In our next lesson, we will build directly on this foundation as we tackle the learning outcome: Apply strict aliasing rules and implement safe type-punning using unions or memcpy. We'll explore the exceptions to the strict aliasing rule and the correct, standard-compliant ways to reinterpret memory from one type to another.