Showing posts with label C. Show all posts
Showing posts with label C. Show all posts

Thursday, January 15, 2026

Nibble to Hexadecimal in 3 Cycles for Java and C

Ignore this post, dear reader.
Everything in it is correct, as far as it goes.
However, it has become abundantly clear that modern CPUs
allow mundane solutions to outperform those presented here.
This post will be revised accordingly, but will likely seem rather pointless.

I’m unsatisfied with Java’s typical bits-to-hex methods (*.toHexString and printf). That’s why—not for the first time—I was examining an issue generally considered settled or unimportant. I hope other malcontents find value in this work.

I created this nibble-to-hex conversion early in January 2025 (and, perhaps, one or more times in preceding years, without thinking twice about it). There is no telling how many programmers invented this before me, so I claim only its independent invention. That said, if these algorithms are new to you, I’d appreciate credit (a line comment will suffice). Even in January of 2026 (the time of publication), Gemini Pro was unaware of precedents for these algorithms, and it confirmed they are optimal. (No alternate approach uses fewer cycles, and no variant can simultaneously convert a sequence of nibbles.)

Returning to the subject…

Store a nibble in an “int” named “nibble” (its high 28 bits must be clear) then use one of the following expressions to obtain the corresponding ASCII/ISO-8859-1/Unicode code-point.

Nibble to Uppercase Hex Notation (3 Cycles)

(char) ((nibble | 0x30) + (9 - nibble >>> 29))

Nibble to Lowercase Hex Notation (4 Cycles)

(char) ((nibble | 0x30) + (9 - nibble >> 31 & 39))

That’s all there is to it. Java methods with detailed documentation, and C ports, are below the fold.

Those cycle counts are correct for the x86 and ARM64 architectures. Both depend on immediate operands, but only x86 benefits from instruction-level parallelism. Unlike x86, the ARM64 SUB instruction does not support an immediate minuend, so loading operand 9 prevents replication of the x86 parallelism. Instead, ARM64 saves a cycle at the end by using a “shifted-add” instruction to perform the logical right shift and addition in one cycle. That, I gather, is a trick x86 cannot perform. So, both architectures show limitations here, but they cancel-out.

(I spent many years programming in assembly on a daily basis, but that time has passed. I still look at microarchitecture documentation, but that does not produce an understanding of instruction sets and architectures comparable to someone who actually writes assembly code for the architectures. So, now, I double-check my conclusions with Gemini Pro.)


Java Method Implementations

private static char nibbleToHexDigitUc
(
    final int               nibble
){
//  Repetition of the following expression may appear tedious, and invites mistakes,
//  hence this method. The expression is short enough to ensure inlining.

/*  Clause 1, "nibble | 0x30" (equivalent to "nibble + '0'" in US-ASCII), produces
    "int" value "c1", ranging from 0x30 ('0') for "nibble" 0x0, to 0x3F ('?') for
    "nibble" 0xF.

    Clause 2, "9 - nibble", differentiates nibbles represented by US-ASCII
    characters '0'-'9' from those represented by 'A'-'F':
        (A) non-negative when "nibble" ≤ 9, or
        (B) negative when "nibble" ≥ 0xA.

    Clause 3, "c2 >>> 29", uses the high 3 bits of "c2" to produce "int" value "c3",
    the "c1" addend either converting US-ASCII characters ':'-'?' (produced from
    nibbles ≥ 0xA) to characters 'A'-'F', or leaving characters '0'-'9' unchanged:
        (A) 0 from "c2A", or
        (B) 7 from "c2B".
    Because "c3" is either 0, or a sequence of consecutive 1 bits, this clause is
    not adaptable to lowercase hex production, which would require a "c3B" value
    of 39 (0b100111).

    Clause 4, "c1 + c3" produces "int" value "c4": the US-ASCII code-point of the 
    hexadecimal digit representing "nibble".

    Note: The whole expression allows two instances of instruction-level parallelism:
          clauses 1 and 2. That will not provide a great performance gain, because
          every operator is single-cycle, and eliminating one cycle is good, but not
          great. Nonetheless, some parallelism is better than none, and executing
          50% of its operators in parallel is better than most expressions.
*/
    //noinspection MagicNumber
    return (char) ((nibble | 0x30) + (9 - nibble >>> 29));
}
private static char nibbleToHexDigitLc
(
    final int               nibble
){
//  Repetition of the following expression may appear tedious, and invites mistakes,
//  hence this method. The expression is short enough to ensure inlining.

/*  Clause 1, "nibble | 0x30" (equivalent to "nibble + '0'" in US-ASCII), produces
    "int" value "c1", ranging from 0x30 ('0') for "nibble" 0x0, to 0x3F ('?') for
    "nibble" 0xF.

    Clause 2, "9 - nibble", differentiates nibbles represented by US-ASCII
    characters '0'-'9' from those represented by 'A'-'F':
        (A) non-negative when "nibble" ≤ 9, or
        (B) negative when "nibble" ≥ 0xA.

    Clause 3, "c2 >> 31", produces "int" value "c3":
        (A)  0 from "c2A", or
        (B) -1 from "c2B".
    Thus, "c3" is an all-or-nothing mask determining whether clause 4 produces an
    addend altering "c1", or 0 which leaves "c1" unchanged.

    Clause 4, "c3 & 39", produces "int" value "c4", the "c1" addend converting
    US-ASCII characters ':'-'?' (produced from nibbles ≥ 0xA) to characters 'a'-'f',
    while leaving characters '0'-'9' unchanged:
        (A)  0 when "nibble" ≤ 9, or
        (B) 39 when "nibble" ≥ 0xA.
    Value "c4B" makes this clause responsible for the letter case of hexadecimal digits
    > 9. As is, hexadecimal digits > 9 are lowercase. Uppercase digits would result
    from replacing 39 with 7 ... *except* there is no point to doing so, because a
    simpler, faster expression produces uppercase hex.

    Clause 5, "c1 + c4" produces "int" value "c5": the US-ASCII code-point of the
    hexadecimal digit representing "nibble".

    Note: The whole expression allows two instances of instruction-level parallelism:
          clauses 1 and 2. That will not provide a significant performance gain
          (every operator is single-cycle, so only one cycle is saved), but 40%
          parallelism is better than none.
*/
    //noinspection MagicNumber
    return (char) ((nibble | 0x30) + (9 - nibble >> 31 & 39));
}

Those are the implementations for Java, my language of choice since the late 1990s. If porting, keep in mind: (1) both expressions implicitly operate on 32-bit, big-endian, two’s-complement integers throughout, (2) operator “>>>” is an unsigned right shift (JLS terminology), otherwise known as a “logical right shift” (typical assembly language terminology), and (3) operator “>>” is a signed (“arithemtic”) right shift.

C Ports (C99 and C23)

C, and, I think, most C-inspired languages, lack an equivalent operator. (Originally, C didn’t specify the sign-extension behavior of its right shift operator, >>, so one might encounter either behavior depending on the CPU and the compiler, although arithmetic [sign-extending] right shifts seem to have been the most common.) Although I spent 15 years programming in C (now designated “K&R C” or “C78”), I never encountered any of C’s standardized versions, and my only C documentation remains the 1978 edition of K&R. However, I gather C99 standardized logical right shift for unsigned integers, so the ports below are deterministic, unless one builds for a CPU that doesn’t use two’s-complement arithmetic on signed integers (apologies to Unisys developers).

Nibble to Uppercase Hex Notation, C99, 3 Cycles

Where “nibble” is of type “int32_t”, its high 28 bits are clear, and two’s-complement arithmetic remains the norm:

(nibble | 0x30) + ((uint32_t) (9 - nibble) >> 29)

Nibble to Lowercase Hex Notation, C99, 5 Cycles

Where “nibble” is of type “int32_t”, its high 28 bits are clear, and two’s-complement arithmetic remains the norm:

(nibble | 0x30) + ((-(int32_t) ((uint32_t) 9 - nibble >> 31)) & 39)

Nibble to Lowercase Hex Notation, C23, 4 Cycles

Where “nibble” is of type “int32_t”, and its high 28 bits are clear:

(nibble | 0x30) + (9 - nibble >> 31 & 39)

Tuesday, December 8, 2009

Parallel Number Crunching – Java and C Source Code

As requested, I've made the source code and sample data for my coplanarity scanning example programs available. These are the programs used to produce the timings shown in my previous post, “Parallel Number Crunching – Java vs. C & Grand Central Dispatch.” Feel free to experiment with them as you wish.

Note that both are hard-coded to break out of their processing loops after the first 5,999,600 planes have been checked for coplanarity with the 30,000 sample points provided in the "data/lc-data-30000.bin" file. That seemed to be a reasonable amount of computation for demonstrating the relative performance of the two implementations. If you want to try to complete the full scan of all possible planes, you'll need to remove that hard-coded break – and then you'll need to wait for a very, very long time.

The more complete Java implementation I've been using for my actual number crunching work has features like saving its processing state periodically, so the program can be quit and later resume running from roughly where it left-off. I've pulled those features out of this example code in order to keep the two implementations similar, small and straightforward.

Here’s wishing you a good night, and good coding.

Sunday, December 6, 2009

Parallel Number Crunching – Java vs. C & Grand Central Dispatch

I've been doing some parallel number crunching lately using Java 1.6 (1.6.0_17) on a MacPro (4,1) with a 2.93 GHz quad-core Nehalem processor. This weekend I decided to brush the dust off my C programming skills and re-implement the core of my Java code in C using Apple's Grand Central Dispatch (GCD) technology to see if I could get a dramatic speed boost. The results were both better and worse than expected.

The number crunching in question was the examination of data from a pseudo-random number generator (PRNG). Specifically, the PRNG output was interpreted as 30,000 triplets of 32-bit signed integers. The triplets were treated as points in 3-space, and the number crunching consisted of the search for every point coplanar with every possible plane those points could define. It's been done before, of course, and found weaknesses in PRNGs like multiplicative congruential generators. For my own edification, I wanted to look for the same weaknesses in some other PRNGs. I have yet to complete a single run, however, so those who've done it in the past either used better techniques than my brute-force approach, had more than my 23 GHz to work with, or were very patient. Maybe some even used worse techniques that were faster. I don't know. In any case, I've been giving the brute-force approach a shot.

My implementation identifies coplanarity as discussed in Wolfram MathWorld: by computing the determinant of a 4X4 matrix. If the determinant is zero, the point is coplanar. The determinant computation includes a lot of multiplies that can be precomputed, because the three points that define the plane don't change for each 30,000 point test, and my code (Java and C) dutifully precomputes the lot. [Some future version of the C code may be able to bring the CPU's vector processing unit, or the GPU's resources, to bear on this problem, but I've never tried vector processing, so that's a challenge I'll leave for another time.]

With that out of the way, here are the results for checking all 30,000 points for coplanarity with each of the first 5,999,600 planes. As you'd expect, lower test times are better.

LanguageEnvironmentTime
Java 1.6 (64-bit)Mac OS X command line811 secs.
Java 1.6 (64-bit)IntelliJ IDEA 7.0.5 console814 secs.
C & GCD (64-bit)Mac OS X command line658 secs.
C & GCD (64-bit)Xcode 3.2.1 debugger console657 secs.
C & GCD (64-bit)Xcode 3.2.1 debugger console with GuardMalloc 18 enabled1,814 secs.

More details: The C compiler was the one supplied with the current Xcode tools, GNU gdb 6.3.50-20050815 (Apple version gdb-1346) configured as "x86_64-apple-darwin". The Java runtime version was 1.6.0_17-b04-248-10M3025 and the virtual machine was "Java HotSpot(TM) 64-Bit Server VM" – in other words it was the current version of Java supplied by Apple for Mac OS X 10.6.2.

The results: Running on a quiescent machine, both the Java and C/GCD applications were able to push the CPU utilization to roughly 795% and keep it there at all times (the "Hyper-Threading" feature of the Nehalem cores enabled each of the four cores to act like two, so 800% was the theoretical maximum in this case). In general, Java was 23% slower than C/GCD. Personally, I'd expected Java to either closely approximate or even slightly exceed C/GCD's performance (because the Java just-in-time compiler can optimize and natively compile the code in a manner optimal to the specific hardware it finds itself running on at any given time, whereas the C compiler might make have to make more generalized assumptions), or for C/GCD to beat Java by many times. Neither outcome manifested, though the first was closest to the truth.

I think that the actual results can be interpreted in a couple of ways: (1) "C with GCD is meaningfully faster than Java for this sort of number crunching", or (2) "Java is a reasonable choice for this sort of number crunching." I lean toward the second interpretation, due to two issues. First, the Java code was, in my opinion, much cleaner, clearer and more re-usable than the C code. Second, Java is doing a lot more work for you at the same time it is crunching numbers – helpful work that can affect the reliability of your code, and/or the integrity of its results, like ensuring memory integrity and runtime type-correctness – and that's work that C mostly doesn't do at all, or does very badly, as seen in the case of running the C version of the program with GuardMalloc enabled at the cost of it taking 2.24 times longer to run than the Java program.

Those issues aside, it must be said that, for this comparatively simple program, Apple's Grand Central Dispatch and its extensions to the C language worked very well, and were easy to use. I still find the new syntax difficult, but past experience with closures, and the examples in Apple's documentation, were enough to let me produce the code I needed, in spite of that. Whether the GCD language extensions, or the GCD API, provide as complete a set of services for concurrent programming as the Java language and its "java.util.concurrent" package, I don't know. The general impression I took away from the GCD documentation was that it provides fewer features to aid the development of concurrent software than does Java, but a day's work with GCD isn't sufficient to let me judge.

To conclude: Compared to C, Java's not half bad for number crunching as I'm currently doing it (in fact, it's somewhere between merely 23% bad, and 224% good); and Apple's Grand Central Dispatch is an interesting and useful extension to the C language.