Delphi Programming

  1. Home
  2. Computing & Technology
  3. Delphi Programming
RTL reference|Glossary|Tips/Tricks|FREE App/VCL|Best'O'Net|Books|Link To
 
Learning Assembler with Delphi
Page 3: A proper example - using asm instructions.
 More of this Feature
• Page 1: What, when, why?
• Page 2: Basic operations
• Page 4: Matrices
• Page 5: Fast screen plotting
 Join the Discussion
"Post your questions, concerns, views and comments to this article..."
Discuss!
 Related Resources
• Delphi Pascal tech. articles
• Win/API programming

   A proper example
Until now we have achieved little substantial. However, all of the basic instructions have been introduced, so let's use them.

Suppose we have to display the output of some function dependant upon two variables. You might imagine this as a three-dimensional map, where the coordinates [x,y] correspond to a height h. When we plot the point [x,y] on the screen we need to give the impression of depth. This can be achieved by using colours of differing intensity, in our example, blue below sea level and green above. What is needed is a function that will convert a given height into the appropriate depth of color for a given sea level.

First we should plan the ranges of the variables involved. It is reasonable to use the integer type to store the height and sea level, given the range of a 32-bit value. The true colour range available, limits us to a maximum color depth for blue and green to 256, so we will have to scale the results accordingly. If we assume a maximum height above, and depth below sea level to be 65536 feet, we can use shl and shr for some fast scaling, and we will get a change in colour depth every 256 feet. The function will of course, have to return a TColor type to be compatible with Delphi.

function ColorMap
  (Height, Sea : integer):TColor; 
begin 
asm 
mov eax,Height 
cmp eax,Sea 
{jump to section dealing with above sea level}
jg @Above


{ Height is beneath Sea Level } 
mov ecx,Sea 
{ ecx is depth beneath sea }
sub ecx,eax
{ divide depth by 256 } 
shr ecx,8
cmp ecx, 256 
{ ecx should not exceed 255 }
jl @NotMaxDepth
mov ecx,255 
@NotMaxDepth: 
{ ecx now holds color } 
jmp @ColorDone 

@Above: 
{ eax is height above sea level } 
sub eax,Sea
{ divide height by 256 }
shr eax,8
cmp eax,256 
{ eax should not exceed 255 }
jl @NotMaxHeight
mov eax,255 
@NotMaxHeight: 
{ eax now holds green color depth } 
{ eax now holds color }
shl eax,8
{ ecx now holds color for
compatibility with beneath
sea level routine} 
mov ecx,eax
@ColorDone: 
mov Result,ecx 
end; 
end; 

As it happens, the above routine can be written with Delphi's assembler directive. This method of writing assembler removes a lot of the protection provided by the complier, but as compensation, speed improves.

Here is the above routine using the assembler directive.

function ColorMap(Height,Sea:integer):TColor;assembler; 
asm 
cmp eax,edx 
jg @Above 
sub edx,eax 
shr edx,8 
cmp edx,256 
jl @NotMaxDepth 
mov edx,255 
@NotMaxDepth: 
mov eax,edx 
jmp @ColorDone 
@Above: 
sub eax,edx 
shr eax,8 
cmp eax,256 
jl @NotMaxHeight 
mov eax,255 
@NotMaxHeight: 
shl eax,8 
@ColorDone: 
end; 

At first sight it is a little difficult to see what's going on. The registers are set with certain values before entering the function or procedure. How these are set depends on how the function or procedure was defined. There are two possibilities.

Stand alone, or explicitly defined procedures and functions
On entry,
eax holds the value of the first parameter of the function or procedure if such exists.
ebx holds the address of the data block of the function or procedure. You must be careful when using ebx, for it must hold its initial value whenever you refer to a function or procedure's parameters or data in your assembler code block. Furthermore ebx must hold its initial value when exiting. The Delphi manual actually says don't touch.
ecx holds the value of the third parameter.
edx holds the second parameter value.

On exit, eax holds the result of the function, or in the case of a procedure, convention states it holds the value of any relevant error code you may define.
ebx must hold its initial value. Failure to ensure this will cause a system crash.

Object method, procedures and functions
On entry,
eax holds the address of the parent object's data block. You don't need to maintain this value, however it is needed whenever you wish to access or change the values of the parent object's fields.
ebx is the same as above.
ecx holds the second parameter value.
edx holds the value of the first parameter.

On exit, the register values are as for a stand alone procedure or function.

With the above information, you should now be able to work your way through the ColorMap function. On the face of it, we seem to have reduced the number of lines of assembler from 18 to 15, which is not much of a saving, and the code is not as readable. However this is not the whole story. The complier generates a fail-safe entry and exit code block for any function or procedure defined with the usual begin..end block. By using the assembler directive, the complier employs only minimal entry and exit code. In the case of the ColorMap function this means it's code size has roughly halved, as has its execution time. These are the levels of performance gain that make writing assembler worthwhile.

Implementing local variables
It is clear, that with just four registers, implementing any half serious algorithm will not be a trivial matter. If a routine is fast, this is usually because every constant used has been calculated before entering any loops. In Object Pascal, this is where local variables are used. For a procedure or function defined with the assembler directive, the same declaration format is used. For example, the following is valid,

function Test:integer:assembler; 
var first,second,third:integer; 
asm 

{some code, remembering that
ebx must be it's initial value}

end; 

Local constants, and constant variables may also be defined just as one would in Object Pascal. The complier just reserves a block of memory, whose address is stored in ebx on entry. Thereafter the local variable names refer to an offset value from the base address of the data block. This allows the complier to use the index addressing provided by the cpu. An index address is of the form [reg1+reg2] or [reg1+exp1] for example [ebx+edx] . You will find indexing the easiest way of addressing data such as strings and arrays but more of this later. In the case of a function definition, a reference to a local variable is implemented as an indexed address irrespective of the operation, consequently the operation mov eax,second is actually mov eax,[ebx+4] where 4 is the offset to the address of the value of the second variable. You could write either, but the former offers greater clarity. I hope you are now starting to appreciate the importance of maintaining the value of ebx.

There is also a quicker method of temporary value storage, and this brings us to the stack.

The Stack
The stack is just what it says. Think of a stack of books on a table, we can place another book on the stack or take a book off the stack. To get at the third book down, we must first remove the top two. This is what we do with register values, and there are two appropriate instructions.

push reg1 place the value of reg1 on top of the stack.

pop reg1 remove the top value of the stack, and place this in reg1.

Windows uses the stack to pass parameters to the api functions, and as such great care must be exercised when using the stack. Each push must have an associated pop in all the code you write, get this wrong and once again the system will crash.

We will use the stack, indeed it is necessary when implementing recursive functions, but generally we shall only use the stack for temporary storage. For example when using the mul and div operations there are register value changes all over the place,

... 
{save edx} 
push edx 
{eax := eax*ecx,
edx holds any overflow}  
mul ecx 
{dump any overflow
value and restore edx}  
pop edx 
... 

An aside on recursion. Avoid it at all costs. Recursive functions may look elegant and appear to use very little code to achieve a great deal, but they are the signature of the inexperienced. A recursive function will commonly overload the stack because at design time you cannot be certain how many recursions are required and as such the memory demands are unknown. How do you explain to an end-user that your 100K program requires 8mb to run, when an equivalent iterative routine may produce a 500K program needing just 1mb. Furthermore, discovering the cause of a stack failure is problematic, making debugging a painful process. The only time I can think of, where recursion is justified, is in the implementation of artificial intelligence algorithms. It might also be noted that generally, I have found iteration to be quicker.

Beyond integers
Until now we have discussed only 32bit integer values. This would seem to impose a limitation on any code we may wish to write, but this is not so. When Object Pascal passes a string as a parameter, for example, it is a 32bit integer value that is used. In this particular case the value passed is the memory address at which the contents of the string are stored. This value is what is referred to as a pointer and is the approach used for any data type, whether string, array or user-defined object. Indeed, even floating point values are referred to in this way, as they are considered to be strings.

What's in a pointer
In a normal pascal procedural definition, the values of the parameters are copied to the data area of the procedure. If the value is an integer the value is passed explicitly whenever the parameter is referred to. In the case of other data structures, the pointer passed, points to the appropriate part of the data area. In neither scenario, is the value of any variable passed as a parameter changed. Object Pascal allows one to define parameters with the var directive, whence pointers to the actual variables are passed. Consequently, changes to actual variable values may be made. The same result is achieved by defining parameters to be of pointer type, each pointer pointing to the appropriate data structure. This latter approach has the advantage of coping with dynamically instantiated data, the price to pay being that care with null pointers and memory allocation should be taken.

Next page > Matrices > Page 1, 2, 3, 4, 5

All graphics (if any) in this feature created by Zarko Gajic.

 More Delphi
· Learn another routine every day - RTL Quick Reference.
· Download free source code applications and components.
· Talk about Delphi Programming, real time.
· Link to the Delphi Programming site from your Web pages.
· Tutorials, articles, tech. tips by date: 2001|2000|1999|1998 or by TOPIC.
· NEXT ARTICLE: Searching for data - DB/9.
Chapter nine of the free Delphi Database Course for beginners. Walking through various methods of data seeking and locating while developing ADO based Delphi database applications.
 Stay informed with all new and interesting things about Delphi (for free).
Subscribe to the Newsletter
Name
Email

 Got some code to share? Got a question? Need some help?

Explore Delphi Programming

About.com Special Features

Build Your Own Website

Step-by-step advice on how to do everything from choosing a Web host to promoting your content. More >

Connect Your Home Computers

Easy ways to connect two computers for networking purposes. More >

Delphi Programming

  1. Home
  2. Computing & Technology
  3. Delphi Programming

©2009 About.com, a part of The New York Times Company.

All rights reserved.