- Mastering Delphi Programming:A Complete Reference Guide
- Primo? Gabrijel?i?
- 1383字
- 2021-06-24 12:33:37
The magic of pointers
Our friend, Mr. Smith, has progressed a lot from his first steps in Delphi. Now he is playing with graphics. He wants to build a virtual reality app that would allow you to walk through the Antarctica forests. While he's getting some success, he has problems displaying the correct colors on the screen.
A part of his code is producing textures in Blue-Green-Red (BGR) byte order, while the graphics driver needs them in the more standard Red-Green-Blue (RGB) order. He already wrote some code to fix this problem, but his solution is a bit too slow. He'd like to push a frame or two more from the system and so I promised that I'd help him optimize the converter. I'd be glad to do it, as his problem neatly fits into the story of pointers.
A pointer is a variable that stores an address of some data (other variables, dynamically allocated memory, specific character in the string ...). It is always of the same size, 4 bytes on 32-bit systems and 8 bytes on 64-bit systems—just the same as NativeInt and NativeUInt. This is not a coincidence as converting a pointer into an integer or back is sometimes quite practical.
Let's say, for example, that we have a TPoint3 record containing a three-dimensional point. We can then declare a pointer to this type with the syntax ^TPoint3 and we can give this type a name, PPoint3. While the size of TPoint3 is 24 bytes, the size of PPoint3 is 4 or 8 bytes, depending on the compiler target (32- or 64-bit):
type
TPoint3 = record
X,Y,Z: double;
end;
PPoint3 = ^TPoint3;
If we now declare a variable P3 of type TPoint3 and variable PP3 of type PPoint3, we can then change the contents of PP3 to the address of P3 by executing PP3 := @P3. After that, we can use the PP3^ notation to refer to the data that PP3 is pointing to (that is P3).
Delphi also allows a shorter form to access fields via a pointer variable. We don't have to write PP3^.X; a simpler PP3.X is enough:
var
P3: TPoint3;
PP3: PPoint3;
begin
PP3 := @P3;
PP3^.X := 1;
// P3.X is now 1
PP3.Y := 2;
// P3.Y is now 2
P3.Z := 3;
// PP3^.Z is now 3
end;
Unlike the original Pascal, Delphi allows some arithmetical operations on the pointer. For example, you can always do Inc(ptr) and that will increment the address stored in ptr. It will not increment it by 1, though, but by the size of the type that the ptr is pointing to.
Some types, such as PChar, PAnsiChar, and PByte, will also allow you to use them in some basic arithmetic expressions. In reality, all pointer types support that, but only pointer types that were defined when the compiler directive {$POINTERMATH ON} was in effect will allow you to do that by default. For other types, you have to enable {$POINTERMATH ON} in the place where you do the calculation.
In the following example, the code pi := pi + 1 wouldn't compile without explicit {$POINTERMATH ON} as this directive is not present in the System unit where PInteger is defined:
procedure TfrmPointers.btnPointerMathClick(Sender: TObject);
var
pb: PByte;
pi: PInteger;
pa: PAnsiChar;
pc: PChar;
begin
pb := pointer(0);
pb := pb + 1;
ListBox1.Items.Add(Format('PByte increment = %d', [NativeUInt(pb)]));
pi := pointer(0);
{$POINTERMATH ON}
pi := pi + 1;
{$POINTERMATH OFF}
ListBox1.Items.Add(Format('PInteger increment = %d', [NativeUInt(pi)]));
pa := pointer(0);
pa := pa + 1;
ListBox1.Items.Add(Format('PAnsiChar increment = %d', [NativeUInt(pa)]));
pc := pointer(0);
pc := pc + 1;
ListBox1.Items.Add(Format('PChar increment = %d', [NativeUInt(pc)]));
end;
Pointers are very useful when you are creating dynamic data structures, such as trees and linked lists, but that is a bit beyond the scope of this book. They are also important when you want to dynamically allocate records. We'll cover that in the next chapter. And they are very handy—as you'll see immediately—when we are processing data buffers.
Let us return to Mr. Smith's problem. He has a graphic texture, organized as an array of pixels. Each pixel is stored as a cardinal value. The highest byte of a pixel contains the Transparency component (also known as Alpha), the next byte contains the Blue component, the byte after that the Green component, and the lowest byte contains the Red component:
Mr. Smith has already created a small test code which you can find in the demo Pointers. The code creates an array with 10 million elements and fills it with the value $0055AACC. The Blue color is set to $55, Green to $AA, and Red to $CC for all pixels.
The code then walks over the array and processes each element. Firstly, it extracts the Blue component by masking only one byte (AND $00FF0000) and moving it 16 bits to the right (SHR 16). Then it extracts the Red component (AND $000000FF). At the end, it clears both Blue and Red out of the original data (AND $FF00FF00) and ORs the Red and Blue components back in. Red is now shifted left by 16 places (SHL 16) so that it will be stored in the second-highest byte:
function TfrmPointers.PrepareData: TArray<Cardinal>;
var
i: Integer;
begin
SetLength(Result, 100000000);
for i := Low(Result) to High(Result) do
Result[i] := $0055AACC;
end;
procedure TfrmPointers.btnArrayClick(Sender: TObject);
var
rgbData: TArray<Cardinal>;
i: Integer;
r,b: Byte;
begin
rgbData := PrepareData;
for i := Low(rgbData) to High(rgbData) do
begin
b := rgbData[i] AND $00FF0000 SHR 16;
r := rgbData[i] AND $000000FF;
rgbData[i] := rgbData[i] AND $FF00FF00 OR (r SHL 16) OR b;
end;
end;
This code runs quite fast, but all that bit operations take some time. How can we speed it up?
The best approach is just to ignore the concept that a pixel is a Cardinal and treat each color component as an individual byte. We can then point one pointer to the Red component of the first pixel and another to the Blue component and just swap the values. Then we can advance each pointer to the next pixel and repeat the process.
To do that, we have to know at least a few things about how Delphi stores integer values in memory. The simple answer to that is—as the CPU expects them. Intel processors are using a little-endian format (yes, that is a technical term) which defines that the least important byte is stored in memory first (little end first).
If we put three pixels into memory so that the first starts at address 0, they would look just as in the image here:
Our improved code uses that knowledge. It uses two pointers, both of type PByte (a pointer to a byte value). The pBlue pointer points to the current Blue byte and the pRed pointer points to the current Red byte. To initialize them, the code sets both to the address of the first pixel (which is the same as the address of its lowest byte) and then increments pBlue by 2 bytes.
In the loop, the code simply stores current the Red pixel into a temporary value (r := pRed^), copies the Blue pixel into the Red pixel (pRed^ := pBlue^) and stores the temporary Red pixel into the Blue location (pBlue^ := r). It then increments both pointers to the next pixel:
procedure TfrmPointers.btnPointerClick(Sender: TObject);
var
rgbData: TArray<cardinal>;
i: Integer;
r: Byte;
pRed: PByte;
pBlue: PByte;
begin
rgbData := PrepareData;
pRed := @rgbData[0];
pBlue := pRed;
Inc(pBlue,2);
for i := Low(rgbData) to High(rgbData) do
begin
r := pRed^;
pRed^ := pBlue^;
pBlue^ := r;
Inc(pRed, SizeOf(rgbData[0]));
Inc(pBlue, SizeOf(rgbData[0]));
end;
end;
The code is a bit more convoluted but not that harder to understand, as all that pixel operations (AND, SHR ...) in the original code weren't easy to read either. It is also much faster. On my test computer, the original code needs 159 ms, while the new code finishes in 72 ms, which is more than twice the speed!
To make a long story short—pointers are incredibly useful. They also make the code harder to understand, so use them sparingly and document the code.