In Easy String Encryption Using CryptoAPI in C++ I described how to encrypt text and recommended that the encrypted text be stored as a series of hexadecimal digits -- because cyphertext may contain embedded NULLs or other characters that can cause processing problems.
Since then I've worked on an application in which large blocks of binary data must be encoded into hexadecimal rapidly, and when needed, decoded back to binary just as rapidly. In the earlier article, I provided a sort of "throw-away" hex encoder/decoder, but I felt that I needed something that was more efficient for production work.
In this article, I'll explore some hex encode/decode options, including the one supported by the CryptoAPI, and at the end of the article, I'll present the ultra-fast functions that ended up in my production code.
Observe the ordering of characters in an ASCII Table The only real issue with hex encoding is that there is a gap between the 9 and the A. Most of the complications come when dealing with that.
How to Encode to Hexadecimal
Your binary source byte is 8 bits. Take the data 4 bits at a time (values 0-15). If that value is 0-9, then just add '0' (0x30). If the value is 10-15, then add 0x37 instead. That gives you a hex digit ('0', '1',...,'9','A',..., 'F'). Output the resulting high hex digit followed by the low hex digit.
Here's a more concise (but slower) version that uses a printf-type tool:
Both of these are usable, but not as efficient as I'd like. The former requires calculations and conditional tests twice for each byte. The latter makes a function call for each byte. Both versions append the output to an ever-growing CString object, and that can be a significant problem when the output is large. Let me explain...
Significant Problem with Long Strings
Each time you concatenate to an existing CString (or an STL string object), the code must check to see if that current allocation is large enough. If not, it must allocate a new chunk that is larger, and copy the existing contents to that location before appending the new text. Imagine how clumsy that gets when the string is about 1MB long!
We'll look at the efficient version that avoids these problems in a minute.
How to Decode from Hexadecimal
Your textual source data is composed of pairs of hex digits, for instance "72" or "0F' or "D3". Take the first digit and subtract '0' (0x30). If the result is greater than 9 (it's 'A','B',...,'F'), then subtract 7, resulting in a value between 0 and 15. Do the same for the second digit. Multiply the first by 16 and add it to the second. Output the resulting 8-bit value.
Here's another try that uses sscanf from the C runtime library:
The CryptoAPI Hex Conversion Functions
None of the above routines provided the kind of speed that I was hoping to achieve. After some research, I found a couple of functions in the CryptoAPI toolkit that convert to and from hex:
CryptBinaryToString and CryptStringToBinary
and I tried them:
Oddly, the only useful option is CRYPT_STRING_HEX -- the CRYPT_STRING_HEXRAW option is no longer supported. The output from CryptBinaryToString appears to be formatted for screen output -- spaces between each pair of hex digits, embedded tab characters, CRLF at the end, etc. The two functions above do work, and there certainly is value in using standard API functions, but the performance was lackluster, and I didn't want the overhead of the extra embedded characters.
- "" title="Finally -- The Ultra-Fast Code I Promised!"]
If you look at the assembly-language output from any of the above routines, you will see conditional jumps and a significant number of calculations. Modern processors are fast, using multiple pipelines to handle conditionals, but there is nothing faster than using a lookup table -- especially when the table is in L1 processor cache... the table access is nearly as fast as register access!
So...
For binary-to-hex, I used a table that contains all possible output values for any byte of data. This is very straightforward -- the data itself is the index into the table. I use a WORD pointer to grab 16 bits (two hex digits) at a time.
For hex-to-binary, I realized that I could use a lookup table that contains all possible hexadecimal digit values. Rather than compensate for the "gap" between '9'' and 'A', I just left empty spots in the table. As a bonus, I left an extra gap between "F" and "a" -- so that the same routine could convert the hexadecimal data regardless of whether it contains A,B,C,D,E,F or a,b,c,d,e,f.
Notes:
- In the final (ultra-fast) ToHex function, note the usage of s.GetBuffer() and s.ReleaseBuffer(). The function allocates the entire output buffer before it starts. This avoids that Significant Problem with Long Strings that I mentioned earlier. I could have allocated a separate buffer and set the result string to it as a final step, but that's one (possibly large) memory copy that I wanted to avoid. By using GetBuffer, I allocate and "freeze" the CString buffer while I'm filling it using high-speed pointer operations.
- All of the above functions would be safer if they had some error handling. For instance, the FromHex functions all assume that the output buffer is large enough to hold the resulting binary data. I omitted that but you might want to be more careful. Note the technique used in the CryptoAPI functions, and emulate what Microsoft has done.
- Also, none of the FromHexXxxx routines take any special action when encountering an invalid hex digit; in fact, the fastest one may fail with an access violation if it indexes beyond the end of the lookup table. In my case (and to avoid complicating this article) I assumed that I'd always have valid hexadeximal data. You may not want to make that assumption.
- The ToHexManual, ToHexSlow, FromHexManual, and FromHexSlow all assume that the letter digits are uppercase (ABCDEF). The FromHexXxxx versions of these will fail if lowercase hex digits are found. The CryptoAPI functions use lowercase.
The ToHex (fast) function generates lowercase letter-digits (abcdef) and FromHex (fast) will work with either uppercase or lowercase. If you want uppercase output from ToHex, you can select the entire table in Visual Stuido, then use Edit/Advanced/Make Uppercase (If you want the option of using either, just create two tables).
- The timing test results are interesting. I read in a large (1MB) binary file (a ZIP file), then started the timer and ran 10 iterations of each function.
All times are in milliseconds, based on the less-than-precise GetTickCount() function. I also ran a 100-iteration test on each to verify and got similar performance ratios. The really big numbers for ToHexManual and ToHexSlow are almost certainly caused, at least in part, by the Significant Problem with Long Strings issue (which they both use).
The FromHexManual speed was faster than I thought it would be. Upon examining the compiler-generated ASM source code, I found that the compiler had "inlined" the function. Also, it seems that the calculate-and-jump-conditionally sequence is handled very efficiently on my AMD-based PC -- it's only about 50% slower than the lookup-table technique.
In any case, the lookup-table versions proved to be significantly faster than the others.
References:
CryptBinaryToString Function
http://msdn.microsoft.com/
CryptStringToBinary Function
http://msdn.microsoft.com/
How fast is your ASM hex converter?
http://www.experts-exchang
This Experts-Exchange "question" was really a challenge to create a particular kind of hexadecimal output in intel ASM. The lookup table technique won out easily... until some smarty pants!!! figured out how to use MMX opcodes and handle larger chunks of data. Interesting reading, and a lot of fun for the participants!
=-=-=-=-=-=-=-=-=-=-=-=-=-
If you liked this article and want to see more from this author, please click the Yes button near the:
Was this article helpful?
label that is just below and to the right of this text. Thanks!
=-=-=-=-=-=-=-=-=-=-=-=-=-
by: Qlemo on 2009-08-09 at 05:36:31ID: 2609
Astonishing result. I had expected the string versions to be slow, for obvious reason.
Coming from the old days of 3.5KB "PCs" and programmable calculators, I never would have considered the lookup table approach to be that speedy and hence useful. I reckon I should start thinking in bigger figures (i.e. GB) :-)