Aussie AI

What is Pointer Arithmeticand?

  • Book Excerpt from "Generative AI in C++"
  • by David Spuler, Ph.D.

What is Pointer Arithmetic?

Pointer arithmetic is a tricky C++ optimization that can be used to get rid of incremented variables in loops. Instead, a pointer can be incremented each loop iteration. This changes an array access “arr[i]” into a pointer access “*ptr” and is usually faster.

What is pointer arithmetic? Arrays and pointers are buddies in C++ and there's a way that mathematical arithmetic operators can work on both. Consider the declarations:

    int arr[10];
    int *ptr;

To start with, we can set the pointer at the array, and C++ allows us to use index notation on a pointer:

    ptr = arr;
    x = ptr[3];

Here, x will get the value of arr[3] via ptr[3]. The pointer and array are equivalent. Note that the “&” address-of operator can be optionally used here. We could have written “ptr=&arr” to copy the address, but it's optional.

C++ allows array index accesses on pointers with “ptr[3]” as above. We can also do this using “pointer arithmetic” with the “+” operator and the “*” pointer de-reference operator:

    x = *(ptr + 3);  // Same as ptr[3]

The expression “ptr+3” is the address of the third element in the array (i.e., &arr[3]), and the “*” dereference operator gets the value pointed to by the pointer (i.e. arr[3]).

Why does this work? If ptr is pointing to the start of an integer, shouldn't “ptr+3” be a weird address in the middle of an integer?

No, because C++ does “pointer arithmetic” on pointers. Because “ptr” is an “int*” type pointer, the compiler knows to work on “int” data. With pointer arithmetic, the “+” operation adds a multiple of the bytes of the size of int types. So “ptr+1” is not the address 1 more than ptr, it's actually 4 more than ptr for a 4-byte int (assuming 32-bit integers). And “ptr+3” is actually the address “ptr+12” in terms of bytes.

Which Operators Do Pointer Arithmetic? Pointer arithmetic works with a number of arithmetic operators:

  • Increment — ptr++ adds 1*size bytes to ptr.
  • Decrement — ptr-- subtracts 1*size bytes from ptr.
  • Addition — ptr + n adds n*size bytes.
  • Subtraction — ptr-n subtracts n*size bytes.
  • Assign-Add — ptr += n adds n*size bytes to ptr.
  • Assign-Subtract — ptr -=n subtracts n*size bytes from ptr.

Note that there's no pointer arithmetic multiplication or division. Actually, I was told that C++37 was going to have a C++ pointer multiplication operator that scanned down an array doing paired multiplications, adding them up as it went, and all in one CPU cycle, but then someone woke me up.

Pointer Comparisons: You can also compare pointers, which isn't really doing any special pointer arithmetic, but works as normal comparisons on their addresses:

  • Equality tests — ptr1 == ptr2 or ptr1 != ptr2
  • Less than — ptr1 < ptr2 or ptr1 <= ptr2
  • Greater than — ptr2 > ptr2 or ptr1 >= ptr2

Segmented Memory Model Pointer Comparisons: Note that there's a weird portability gotcha in relative pointer comparisons (i.e. less-than or greater-than). They're only guaranteed to work in very limited scenarios by the C++ standard, such as when the pointers are both operating over the same array data. Programmers tend to think of the address space as one huge contiguous range of addresses, where you can compare all of the pointers in the program against each other, and make some coding assumptions based on that. However, there are architectures where pointer addressing is more complicated, such as where pointers are a multi-part number pointing into different memory banks with a more convoluted segmented addressing scheme. For example, pointers to allocated heap memory might be separate from the pointers to global static data, and not easily comparable.

Pointer Differences: You can subtract two pointers using the normal “-” subtraction operator. The result is not the number of bytes between them, but the number of objects. Hence, the two pointers must be of the same type (i.e., pointing to the same type of object). Consider this code:

    int arr[10];
    int *ptr1 = &arr[1];
    int *ptr2 = &arr[2];
    int diff = ptr2 - ptr1;

The value of “diff” should be 1 in C++ (rather than 4 bytes), because the two pointers are one element apart (i.e. 1 integer difference). Note that “diff” is a signed integer here, and the value of subtracting two pointers can be negative (e.g. “ptr1-ptr2” above would be “-1” instead). Technically, the official type of the difference between two pointers is “std::ptrdiff_t” which is an implementation-specific integral signed type that you can use if you are the sort of person who alphabetizes their pantry.

Adding Pointers Fails: Note that adding two pointers with “ptr1 + ptr2” is meaningless and usually a compilation error. Also invalid are weird things like the “+=” or “-=” operators on two pointers. Even though “-” is valid on two pointers, “ptr1-=ptr2” fails to compile because the result of “ptr1-ptr2” is a non-pointer type.

Char Star Pointers (Size 1 Byte): Note that if you want to avoid pointer arithmetic, and see the actual numeric value of addresses, you can use a “char*” type pointer (or “unsigned char*”). Since sizeof(char) is 1 byte, then all of the pointer arithmetic will just add the expected number of bytes (e.g. ptr++ on a char* pointer adds 1 to the address). If you want to know the actual number of bytes between two pointers, then cast them to “char*” type before doing the pointer subtraction.

    int diffbytes = (char*)ptr2 - (char*)ptr1;

Stride of an Array. A useful piece of terminology when processing lots of AI model data in memory is the “stride” of an array. This means the number of bytes between adjacent array elements. We can try to compute it as follows:

    int arr[100];
    int stride = &arr[2] - &arr[1];  // Wrong

Nope, that's a fail. This isn't the stride, because it did pointer arithmetic. The addresses of array elements are really pointers, so the stride variable above is always 1 (the adjacent elements are 1 apart in pointer arithmetic). We need to convert to char pointers to get the stride in bytes.

    int arr[100];
    int stride = (char*)&arr[2] - (char*)&arr[1];

Can't we just use sizeof to get the stride? Isn't the stride above going to equal 4, which is sizeof(int)? Yes, in the example above the use of sizeof is correct, but no, that is not true in general. The stride will often equal the element size, but may be larger. For a simply packed array of integers or other simple types, the stride is almost certainly the size of the array element type. But this is not always true, such as if it's an array of a larger object with an awkward size that requires padding bytes for address alignment considerations.

Loop Unrolling Stride. The term “stride” also has a secondary meaning when talking about array processing with loop unrolling. The stride of an unrolled loop is how long of a segment is being processed in each section of loop unrolling code. For example, if a loop is unrolled with AVX-2's 256-bit registers (equals 8 32-bit floats), then the stride when discussed in the literature is either 8 floats or 8x4=32 bytes.

Void Pointer Arithmetic Fails: Note also that pointer arithmetic on a generic “void*” pointer should be a compile error, because it points to unknown size objects. Some C++ compilers will allow pointer arithmetic on void pointers with a warning, and pretend it's a “char*” pointer instead.

Finally, I don't think you can increment a “function pointer” in valid pointer arithmetic, but you're welcome to try.

 

Next:

Up: Table of Contents

Buy: Generative AI in C++: Coding Transformers and LLMs

Generative AI in C++ The new AI programming book by Aussie AI co-founders:
  • AI coding in C++
  • Transformer engine speedups
  • LLM models
  • Phone and desktop AI
  • Code examples
  • Research citations

Get your copy from Amazon: Generative AI in C++