Five Low Words
Consider the simple LoWord function, which extracts the low word of a DWord. What could be simpler? Just mask out the high byte of the argument and return the result:
Function LoWord1(ByVal dw As Long) As Integer
LoWord1 = dw And &HFFFF&
End Function
This works fine as long as the low word is less than or equal to 32767. But now try this:
w = LoWord1(32768)
You get an overflow because 32768 is out of the integer range (-32768 through 32767). If you’re accustomed to another language, you might expect that you could use a conversion function to force the assignment to an integer. Basic conversion functions such as CInt, however, don’t force anything. They request politely. If you change the guts of LoWord as shown here, you get the same overflow error:
LoWord1 = CInt(dw And &HFFFF&)
In most languages, type conversion or casting means “force by wrapping, truncation, or whatever it takes.” In Basic, it means “Mother, may I?” And in this
case, the answer is always “No.” You might argue that any Long from -32768
through 65535 can legitimately be converted to an Integer, and I’ll agree with you. But we lose.
In order to do the right thing for all integers, you must specifically check for the sign bit:
Function LoWord2(ByVal dw As Long) As Integer
If dw And &H8000&) Then
LoWord2 = dw Or &HFFFF0000
Else
LoWord2 = dw And &HFFFF&
End If
End Function
Checking for the sign bit takes time; this version of LoWord is a few ticks slower than the previous one. But remember Joe Hacker’s motto: “It doesn’t matter how fast your code is if it doesn’t work.”
There’s more than one way to get a DWord in Basic. You could simply copy the bits of the low word to the result. Basic provides a roundabout method of doing this with the LSet statement. You have to set up two structures that split the data in different ways:
Private Type TLoHiLong
lo As Integer
hi As Integer
End Type
Private Type TAllLong
all As Long
End Type
Then you write the data into one structure and read it out of the other:
Function LoWord3(ByVal dw As Long) As Integer
Dim lohi As TLoHiLong
Dim all As TAllLong
all.all = dw
LSet lohi = all
LoWord3 = lohi.lo
End Function
This code looks complicated, but internally it’s not doing a lot. This version is slower than the LoWord2 version, but it has the advantage of always working at the same speed, regardless of the number passed to it.
There’s another way to copy bits using the Windows API CopyMemory function. See the sidebar “CopyMemory: A Strange and Terrible Saga” in Chapter 2 for the bizarre story of what CopyMemory really is. Here’s the code to simply copy the contents of the low word to a separate word-sized variable:
Function LoWord4(ByVal dw As Long) As Integer
CopyMemory LoWord4, dw 2
End function
I expected this code to beat the LSet version, but it usually came out a little slower. While LoWord is short and sweet, HiWord is a bit more difficult, as you will soon see.
Keep in mind that this version works only on Little Endian systems such as the Pentium. Visual Basic is now available on machines running Digital’s Alpha chip, which is a Big Endian system. Endian refers to the order in which bytes are stored. The term is taken from a story in Gulliver’s Travels by Jonathan Swift about wars fought between those who thought eggs should be cracked on the Big End and those who insisted on the Little End. With chips, as with eggs, it doesn’t really matter as long as you know which end is up.
The Big Endian version of LoWord4 would probably look like the following code:
Function LoWord4(ByVal dw As Long) As Integer
CopyMemory LoWord4, ByVal VarPtr(dw) + 2, 2
End Function
That’s what HiWord looks like on Little Endian systems, so the Big Endian HiWord would probably look like the Little Endian LoWord. I didn’t have an Alpha machine to test this on. I don’t know if Alphas even have a CopyMemory equivalent. If you’re fortunate enough to develop on an Alpha machine, you’ll have to check carefully all the hacks I do with CopyMemory. If you have problems, it should be easy to fix them with conditional compilation.
Face it, Bit bashing isn’t Basic’s strong point. Here’s what the same function looks like in C++:
WORD DLLAPI LoWord(DWORD dw)
{
return (WORD) (dw & OxFFFF);
}
The C++ version is simpler, but it turns out to be slightly slower (probably because of the cost of calling it through a DLL). But even if C++ bit bashing were faster, the difference wouldn’t justify adding an extra DLL. The official LoWord in the VBCore component and in BYTES.BAS has the same code shown in LoWord2 above. It’s fast enough.
PERFORMANCE
Problem: Compare several methods of stripping the low word of a DWord.
Problem |
P-Code |
Native Code |
AND positive low word |
.0191 sec |
.0016 sec |
AND negative low word |
Overflow |
Overflow |
AND positive low word after sign check |
.0226 sec |
.0017 sec |
OR negative low word after sign check |
.0228 sec |
.0016 sec |
Copy low word with LSet |
.0232 sec |
.0033 sec |
Copy low word with CopyMemory |
.0259 sec |
.0068 sec |
AND low word in C++ |
.0084 sec |
.0024 sec |
Conclusion: It’s a close race with p-code but no contest with native code.
A compiler covers a multitude of sins. Realistically, of course, that extra speed isn’t noticeable except in the most deeply nested of loops.
Not only is the C++ version easier to write, it’s also faster. Realistically, of course, that extra speed is seldom noticeable except in the most deeply nested loops. It’s certainly not worth adding a C++ DLL just for this function (or its relatives). In fact, if you check the performance sidebar, you can see that I have been
cheating—comparing the p-code versions instead of the compiled versions. The version in the VBCore component (or BYTES.BAS) is actually the one that uses CopyMemory. It’s fast enough.