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++
adds1*size
bytes toptr
. - Decrement —
ptr--
subtracts1*size
bytes fromptr
. - Addition —
ptr + n
addsn*size
bytes. - Subtraction —
ptr-n
subtractsn*size
bytes. - Assign-Add —
ptr += n
addsn*size
bytes toptr
. - Assign-Subtract —
ptr -=n
subtractsn*size
bytes fromptr
.
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
orptr1 != ptr2
- Less than —
ptr1 < ptr2
orptr1 <= ptr2
- Greater than —
ptr2 > ptr2
orptr1 >= 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 float
s),
then the stride when discussed in the literature is either 8 float
s 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 |
The new AI programming book by Aussie AI co-founders:
Get your copy from Amazon: Generative AI in C++ |