A decent compromise between readability and execution speed is this:
#include <stdint.h>
#include <string.h>
char* i32tostr (int32_t n, char str[32+1])
{
const char NIBBLE_LOOKUP[16][4] =
{
"0000", "0001", "0010", "0011",
"0100", "0101", "0110", "0111",
"1000", "1001", "1010", "1011",
"1100", "1101", "1110", "1111",
};
char* ptr = str;
for(uint32_t bit=32; bit>0; bit-=4)
{
uint32_t shift = bit - 4;
uint32_t mask = 0xFu << shift;
size_t index = (n & mask) >> shift;
memcpy(ptr, NIBBLE_LOOKUP[index], 4);
ptr+=4;
}
*ptr = '\0';
return str;
}
This reads the number 4 bits (a nibble) at a time from MSB to LSB. It masks out a nibble, then does a table look-up to get the pre-calculated string.
As it happens, a 4 byte string can be copied in a single instruction on 32 bit computers. Note the intentional subtle detail: const char NIBBLE_LOOKUP[16][4]
instead of const char* NIBBLE_LOOKUP[16]
. This means that the null terminator in the string literals is not stored and we can't use strcpy
. Instead we use the significantly faster memcpy
.
The local variables in the for loop are there for readability and don't affect performance. I could as well have written it as
for(uint32_t shift=28; shift>0; shift-=4)
{
memcpy(ptr, NIBBLE_LOOKUP[(n & 0xFu<<shift) >> shift], 4);
ptr+=4;
}
But that's much harder to read and yields exactly the same machine code anyway.
In terms of execution speed, this should be much faster than parsing bit by bit and building up a string that way. The x86 disassembly looks pretty good; branch-free and cache-friendly: https://godbolt.org/z/DgJcVC.
Complete example:
#include <stdint.h>
#include <string.h>
char* i32tostr (int32_t n, char str[32+1])
{
const char NIBBLE_LOOKUP[16][4] =
{
"0000", "0001", "0010", "0011",
"0100", "0101", "0110", "0111",
"1000", "1001", "1010", "1011",
"1100", "1101", "1110", "1111",
};
char* ptr = str;
for(uint32_t bit=32; bit>0; bit-=4)
{
uint32_t shift = bit - 4;
uint32_t mask = 0xFu << shift;
size_t index = (n & mask) >> shift;
memcpy(ptr, NIBBLE_LOOKUP[index], 4);
ptr+=4;
}
*ptr = '\0';
return str;
}
#include <stdio.h>
#include <limits.h>
int main (void)
{
char str[32+1];
puts(i32tostr(0,str));
puts(i32tostr(1,str));
puts(i32tostr(-1,str));
puts(i32tostr(INT_MIN,str));
puts(i32tostr(INT_MAX,str));
}
Output:
00000000000000000000000000000000
00000000000000000000000000000001
11111111111111111111111111111111
10000000000000000000000000000000
01111111111111111111111111111111