Make Your Function Modular and Re-Usable
Let’s say you want to call fprintf( stderr, "Error opening file: s.\n", errMsg );
Your function can’t do that, because it only implements printf
. You could copy and paste everything but the code that initializes the output handle to standard output, but that would be silly. What you obviously would do, especially when saving space, is to implement printf( format, ... )
as a wrapper for vfprintf( stdout, format, args )
. Then fprintf()
and vprintf()
are also trivial wrappers for vfprintf
.
Now let’s say you want to add snprintf()
someday. Can you re-use this code? Well, you can copy-and-paste it. But it’s hardcoded to call _WriteFile@20
. So you have to replace that everywhere with a call to strncpy
instead.
But you can instead define the output function as a higher-order type:
/* On Windows, we use low-level file I/O.
*/
#if defined(_WIN32) && !defined(_WIN64)
#include <io.h>
typedef int fd;
#else // Some other OS. Probably POSIX-compatible?
#error "Implement low-level I/O for this platform."
#endif // Platform check.
/* An output function takes a buffer, a count of characters to output, and a
* pointer to a state object specific to that output function, which it
* updates.
*
* Returns the number of bytes written on success, or a negative value on
* failure. Might also set errno.
*/
typedef int (*output_func)( const char*, size_t, void* );
static int printfd_output_func( const char* const buffer,
const size_t n,
void* const p
)
/* Passes a buffer of size n to a low-level output function. The
* pointer argument p is a pointer to a file descriptor, cast to a void*.
*
* Returns n on success, or a negative value on error.
*/
{
#if defined(_WIN32) && !defined(_WIN64)
return _write( *(fd*)p, buffer, n );
#else
# error "Implement printf_output_func()!"
#endif
}
The output_func
type is a function that takes an input buffer, a range of characters to copy, and an arbitrary pointer to some state that can vary. For this example, which outputs to a file descriptor, it can be as simple as a pointer to a file descriptor (or you could even pass the file descriptor as a machine word without going through a pointer conversion, in assembler). For the snprintf
example, the relevant state object might be the current position in the output buffer and the number of bytes remaining.
if you don’t want the overhead of pushing and popping arguments on the stack, many compilers and assemblers for Win32 will let you give this function the __fastcall
convention, which is very similar to the one you used.
This lets the function you actually implement in another source file become something like:
extern int my_output_helper( output_func engine,
void* output_state,
const char* format,
va_list args
);
Essentially all of the standard library can be implemented with this plus a small handful of short output functions. For example, printf
becomes:
int my_printf( const char* const format, ... )
{
fd fd_stdout = 1;
va_list args;
va_start( args, format );
const int written = my_output_helper( printfd_output_func,
&fd_stdout,
format,
args
);
va_end(args);
return written;
}
Whereas fprintf
would instead look up the file descriptor of the FILE*
you pass in as the first argument and pass that to the output_state
, vfprintf
would pass it the va_list
argument it received, and different output functions would support printing to a Windows file handle, a string, a message window and so on.
if you want to save space, don’t rewrite the same function more than once! And, as a nice bonus, you get runtime polymorphism.
Have the Assembler Mangle Names For You
it should let you declare external functions for both the C
calling convention and the STDCALL
convention used by Windows system calls.
There are also .inc
and .lib
header files for the Windows API in MASM.
MASM Compatibility
You write that you haven’t tested with other assemblers, but you expect it to be compatible. Unfortunately, ML.EXE doesn’t support gas-style local labels, such as .start:
.
Consider Writing MSVC-Compatible Code with LLVM
A command line such as
clang --std=c17 -target i386-windows-pc-msvc -march=i686 -Os -Wall -Wextra -Wpedantic -Wconversion -Wdeprecated -o myprintf.exe myprintf.c my_output_helper.obj
will compile code compatible with gcc and link it with the standard MSVC runtime, instead of MINGW’s. (You can also get it to compile for Win32 using MINGW.) There is also a LLVM assembler that should be more compatible with gas than MASM is.
How Serious Are You About Making This Useful?
Time for the frame challenge. You likely already know this and are just reinventing the wheel as a learning exercise. If so, great!
In practice, you’re not going to be able to avoid loading the C runtime in a real-world app. There are some embedded applications where optimizing individual bytes matters, and you would want to link to a smaller replacement for the standard library, but those do not run on top of Win32.
In that case, the 100K or so overhead of the runtime is a cost you’re going to have to pay. And once you do, the standard library runtime (MSVCRT, libc.so, etc.) is something that will always be loaded into system memory anyway. And you’ll eventually want more than 100K worth of features in your version anyway. Using the shared library takes up less space than reimplementing and statically linking it.
You’re also not going to save significant amounts of space writing the function in assembly, as opposed to compiling a portable version implementing a state machine with the optimize-for-space setting. It is, however, a great exercise to learn i386 assembler!
Always Check for Buffer Overruns
Always, always, always! Really.
Currently, despite optimizing for size, you’re allocating a full kibibyte on the stack as a buffer before calling the function, and then never bounds-checking in the function itself.
In this case, you actually can guarantee that three bytes of ASCII per byte of binary integer is enough to hold the decimal digits (since the maximum value of a N-byte unsigned int is 2**8**N = 256**N < 1000**N = 10**3**N), and you can get the size of the input with sizeof(long long int)
. You can prove a tighter bound, 5*N/2, if you want. (Technically: assuming 8-bit bytes, but you’re writing in x86 assembler anyway.)
But it’s a very bad habit to skip security checks because you think you can assume your buffers are big enough. At the very least, pass in the bound of your output buffer and check you don’t exceed it.
Edit:
As an example of how easy it is to make this kind of mistake in C, the first version I posted had a risk in that, if you called u2a
with the start and the end of the buffer switched, the compiler would not notice anything was wrong, but would gleefully underflow and cause a memory-corruption bug. This was because I only checked whether the lower bound of the array was equal to the iterator each time it decremented, not whether it was less than or equal.
I ended up going back and refactoring to always take a fixed-sized buffer, and only generate pointers within that buffer internally. I still left an assertion that the pointer stays within range.
So you can see why I’m so paranoid about buffer overruns in C.
Start from the End of the Buffer and Count Down
Currently, you have two loops that call divq10
, one to count up to the number of digits, and one to count back down to 0. You can simplify this a lot by passing the start and end of the buffer, and filling each digit in at the end. You can then return a pointer to the start of the numeral within the buffer. This also eliminates the need to have a separate divq10
function, since now you only need the code in a single loop, and do not have to call it at all.
Avoid Copying the Format String
Copying to a 1 KiB buffer is not only inefficient in both time and memory, it’s insecure because you never check if your user-provided format string smashes the stack. What you should be doing instead is tracking the range of bytes that you want to echo literally, and flushing it whenever you see a %
sign in the format string. This involves making more system calls, but your stated requirements are that you don’t care about speed, only space.
A Sample Implementation
This is neither optimized for space nor written in assembler, but it could give you a good starting point, in the right compiler.
You can definitely improve on this. It doesn’t support %ll
, but does support %s
and %%
. I added my_snprintf
and my_printf
as examples of two different output callbacks, and put in a lot of error checking.
if, however, you see it as worthwhile to replace part of the generated code by hand-optimized assembly, compiling to assembly (with -S
on most compilers or FAu
on MSVC) and editing the output listings is a good place to begin.
myprintf.h
:
#ifndef MYPRINTF_H_INCLUDED
# define MYPRINTF_H_INCLUDED
/* You might wish to reimplement this as part of your project:
*/
#include <stdarg.h>
#include <stddef.h>
#ifndef EXIT_SUCCESS
# define EXIT_SUCCESS 0
#endif
#if __GNUC__ || __clang__
# define NORETURN __attribute__((noreturn))
#elif _MSC_VER
# define NORETURN __declspec(noreturn)
#else
# define NORETURN /**/
#endif
#ifdef _M_IX86
# define OUTPUT_FUNC __fastcall
#elif __i386 && __GNUC__
# define OUTPUT_FUNC __attribute__((fastcall))
#else
# define OUTPUT_FUNC /**/
#endif
#if __clang__
# define MUSTTAIL __attribute__((musttail))
#else
# define MUSTTAIL /**/
#endif
extern int my_printf( const char* format, ... );
extern int my_snprintf( char* s, size_t n, const char* format, ... );
extern void NORETURN fatal_error(const char* msg);
/* An output function takes a buffer, a count of characters to output, and a
* pointer to a state object specific to that output function, which it
* updates.
*
* Returns the number of bytes written on success, or a negative value on
* failure. Might also set errno.
*/
typedef int (OUTPUT_FUNC * output_func)( const char*, size_t, void* restrict );
#endif /* MYPRINTF_H_INCLUDED */
myprintf.c
:
#include <assert.h>
#include <limits.h> // for INT_MAX
#include <stdlib.h> // For abort
#include <string.h> // For memcpy, strlen
#include "myprintf.h"
/* On Windows, we use low-level file I/O.
*/
#if defined(_WIN32) && !defined(_WIN64)
#include <io.h>
typedef int fd_t;
#define FD_STDOUT (1)
#define FD_STDERR (2)
#else // Some other OS. Probably POSIX-compatible?
#error "Implement low-level I/O for this platform."
#define OUTPUT_FUNC /**/
#endif // Platform check.
#define MSG_OVERFLOW_ERR "Overflow error."
NORETURN void fatal_error(const char* const msg)
{
_commit(FD_STDOUT); // Don't cross the streams!
_write( FD_STDERR, msg, strlen(msg) );
abort();
}
static int OUTPUT_FUNC
printfd_output_func( const char* const buffer,
const size_t n,
void* const restrict p
)
/* Passes a buffer of size n to a low-level output function. The
* pointer argument p is a pointer to a file descriptor, cast to a void*.
*
* Returns n on success, or a negative value on error.
*/
{
#if defined(_WIN32) && !defined(_WIN64)
return _write( *(fd_t*)p, buffer, n );
#else
# error "Implement printf_output_func()!"
#endif
}
typedef struct snprintf_state_t {
char* it;
size_t remaining; // Leave space for the terminatng null!
} snprintf_state_t;
static int OUTPUT_FUNC
snprintf_output_func( const char* const buffer,
const size_t n,
void* const restrict p
)
{
snprintf_state_t *state = p;
const size_t nbytes = (n <= state->remaining) ? n : state->remaining;
if ( nbytes > (unsigned)INT_MAX ) {
fatal_error(MSG_OVERFLOW_ERR);
}
memcpy( state->it, buffer, nbytes );
state->it += nbytes;
state->remaining -= nbytes;
return (int)nbytes;
}
extern int my_output_helper( output_func engine,
void* restrict engine_state,
const char* format,
va_list args
);
int my_printf( const char* const format, ... )
{
fd_t fd_stdout = FD_STDOUT;
va_list args;
va_start( args, format );
const int written = my_output_helper( printfd_output_func,
&fd_stdout,
format,
args
);
va_end(args);
return written;
}
int my_snprintf( char* const s, size_t n, const char* const format, ... )
{
va_list args;
va_start( args, format );
snprintf_state_t state = { .it = s,
.remaining = n-1
};
const int written = my_output_helper( snprintf_output_func,
&state,
format,
args
);
assert((unsigned)written < n); // This SHOULD be redundant.
s[written] = '\0';
va_end(args);
return written;
}
my_output_helper.c
:
#include <assert.h>
#include <limits.h> // For INT_MAX
#include <string.h> // For strlen
#include "myprintf.h"
#define MSG_WRITE_ERR "Write error."
#define MSG_FORMAT_ERR "Invalid format string."
#define MSG_OVERFLOW_ERR "Overflow error."
#define MSG_BOUNDS_ERR "Array access out of bounds!"
#define ITOA_BUF_LEN (sizeof(int)*5U/2U + 2U)
static char* u2a_helper( const unsigned n,
char* const current,
const char* const lower_bound
)
/* Meant to be called only from u2a. Fills in the buffer from back to front,
* stopping if it reaches the start of the buffer (which should not happen).
*/
{
static const unsigned radix = 10;
const unsigned residue = n/radix;
*current = (char)('0' + n%radix);
if (residue > 0) {
assert( current > lower_bound );
MUSTTAIL return u2a_helper( residue, current-1, lower_bound );
}
return current;
}
static char* u2a( const unsigned n,
char buffer[ITOA_BUF_LEN]
)
/* Fills a buffer from back to front with the ASCII representation of
* n. The string is null-terminated.
*
* Returns a pointer to the subrange of the buffer containing the
* numeral.
*/
{
buffer[ITOA_BUF_LEN-1] = '\0';
return u2a_helper( n, &buffer[ITOA_BUF_LEN-2], buffer );
}
int print_unsigned( const unsigned n,
const output_func func_engine,
void* const engine_state
)
/* Prints n as a decimal numeral using the given output callback with the
* provided state. Only returns if there waas no error (although some digits
* might not have been printed).
*/
{
char buffer[ITOA_BUF_LEN] = {0};
const char* const ascii_val = u2a( n, buffer );
const size_t len = ITOA_BUF_LEN - (size_t)(ascii_val - buffer) - 1;
const int result = func_engine( ascii_val, len, engine_state );
if (result < 0) {
fatal_error(MSG_WRITE_ERR);
}
return result;
}
int print_signed( const int n,
const output_func func_engine,
void* const engine_state
)
/* Prints n as a decimal numeral using the given output callback with the
* provided state. Only returns if there waas no error (although some digits
* might not have been printed).
*/
{
char buffer[ITOA_BUF_LEN] = {0};
const unsigned a = (unsigned)((n >= 0) ? n : -n);
char* const ascii_val = u2a( a, buffer );
if (n < 0) {
if (ascii_val == buffer) {
/* Generating a pointer that underflows is technically undefined behavior,
* which some compiler writers consider enough of an excuse to break a
* program and introduce security bugs. Therefore, we must check for
* underflow before decrementing the pointer.
*/
fatal_error(MSG_BOUNDS_ERR);
}
char* const negative_val = ascii_val - 1;
*negative_val = '-';
const size_t len = ITOA_BUF_LEN - (size_t)(negative_val - buffer) - 1U;
const int result = func_engine( negative_val, len, engine_state );
if (result < 0) {
fatal_error(MSG_WRITE_ERR);
}
return result;
} // end if (n < 0)
const size_t len = ITOA_BUF_LEN - (size_t)(ascii_val - buffer) - 1U;
const int result = func_engine( ascii_val, len, engine_state );
if (result < 0) {
fatal_error(MSG_WRITE_ERR);
}
return result;
}
static int fmt_state_literal( const size_t chars_written,
const size_t chars_to_echo,
const output_func func_engine,
void* restrict engine_state,
const char* const format,
va_list args
);
static int fmt_state_percent( const size_t chars_written,
const size_t chars_to_echo,
const output_func func_engine,
void* restrict engine_state,
const char* const format,
va_list args
);
static int fmt_state_width_mod( const size_t chars_written,
const size_t chars_to_echo,
const output_func func_engine,
void* restrict engine_state,
const char* const format,
va_list args
);
static int fmt_state_width_mod( const size_t chars_written,
const size_t chars_to_echo,
const output_func func_engine,
void* const restrict engine_state,
const char* const format,
va_list args
)
{
(void)chars_to_echo; // Suppress unused argument warning.
switch(*format) {
case 'u': {
const int result = print_unsigned( va_arg( args, unsigned int ),
func_engine,
engine_state
);
MUSTTAIL return
fmt_state_literal( chars_written + (unsigned)result,
0,
func_engine,
engine_state,
format + 1,
args
);
}
case 'd': {
const int result = print_signed( va_arg( args, int ),
func_engine,
engine_state
);
MUSTTAIL return
fmt_state_literal( chars_written + (unsigned)result,
0,
func_engine,
engine_state,
format + 1,
args
);
}
default:
fatal_error(MSG_FORMAT_ERR);
}
}
static int fmt_state_percent( const size_t chars_written,
const size_t chars_to_echo,
const output_func func_engine,
void* const restrict engine_state,
const char* const format,
va_list args
)
{
(void)chars_to_echo; // Suppresses warning.
switch (*format) {
case 'l':
static_assert( sizeof(long) == sizeof(int), "" );
/* We do not yet support %lld, %llu, or %ls. */
MUSTTAIL return
fmt_state_width_mod( chars_written,
0,
func_engine,
engine_state,
format + 1,
args
);
case 'h':
/* A short variadic argument is always widened. */
MUSTTAIL return
fmt_state_width_mod( chars_written,
0,
func_engine,
engine_state,
format + 1,
args
);
case 'u': {
const int result = print_unsigned( va_arg( args, unsigned int ),
func_engine,
engine_state
);
MUSTTAIL return
fmt_state_literal( chars_written + (unsigned)result,
0,
func_engine,
engine_state,
format + 1,
args
);
}
case 'd': {
const int result = print_signed( va_arg( args, int ),
func_engine,
engine_state
);
MUSTTAIL return
fmt_state_literal( chars_written + (unsigned)result,
0,
func_engine,
engine_state,
format + 1,
args
);
}
case 's': {
const char* const s = va_arg( args, const char* );
const int result = func_engine( s,
strlen(s),
engine_state
);
if (result < 0) {
fatal_error(MSG_WRITE_ERR);
}
MUSTTAIL return
fmt_state_literal( chars_written + (unsigned)result,
0,
func_engine,
engine_state,
format + 1,
args
);
}
case '%': {
static const char percent_sign[] = {'%'};
const int result = func_engine( percent_sign,
sizeof(percent_sign),
engine_state
);
if (result < 0) {
fatal_error(MSG_WRITE_ERR);
}
MUSTTAIL return
fmt_state_literal( chars_written + (unsigned)result,
0,
func_engine,
engine_state,
format + 1,
args
);
}
default:
fatal_error(MSG_FORMAT_ERR);
}
}
static int fmt_state_literal( const size_t chars_written,
const size_t chars_to_echo,
const output_func func_engine,
void* const restrict engine_state,
const char* const format,
va_list args
)
{
switch(*format) {
case '\0': {
const int result = func_engine( format - chars_to_echo,
chars_to_echo,
engine_state
);
if (result < 0) {
fatal_error(MSG_WRITE_ERR);
}
if ( (unsigned)(INT_MAX - result) < chars_written ) {
/* Signed underflow is undefined behavior, so we must again code
* defensively, and check for it without writing undefined behavior
* that would make some compilers gleefully break the check.
*/
fatal_error(MSG_OVERFLOW_ERR);
}
return (int)chars_written + result;
}
case '%': {
const int result = func_engine( format - chars_to_echo,
chars_to_echo,
engine_state
);
if (result < 0) {
fatal_error(MSG_WRITE_ERR);
}
MUSTTAIL return
fmt_state_percent( chars_written + (unsigned)result,
0,
func_engine,
engine_state,
format + 1,
args
);
}
default:
MUSTTAIL return
fmt_state_literal( chars_written,
chars_to_echo + 1,
func_engine,
engine_state,
format + 1,
args
);
}
}
int my_output_helper( const output_func func_engine,
void* const restrict engine_state,
const char* const format,
va_list args
)
{
return fmt_state_literal( 0, 0, func_engine, engine_state, format, args );
}
And, finally, test_myprintf.c
:
#include <assert.h>
#include "myprintf.h"
int main(void)
{
const int n1 = my_printf( "Message: %s, %s", "hello", "world!" );
my_printf( " (%d)\n", n1 );
const int n2 = my_printf( "1 + 1 = %u, and %hu%% not %lu.", 1+1, 100, 1234567890UL );
my_printf( " (%d)\n", n2 );
const int n3 = my_printf( "%hd minus %d is %ld.", 1, 2000000000, 1 - 2000000000 );
my_printf( " (%d)\n", n3 );
char abc[4] = "xxxx";
my_snprintf( abc, sizeof(abc), "ABC!!! THIS SHOULD NOT PRINT !!!" );
const int n4 = my_printf( "%s and %d.", abc, 123 );
my_printf( " (%d)\n", n4 );
return EXIT_SUCCESS;
}
Let’s take a closer look at the code four major compilers generate for this, on x86. The function we’ll be looking at is u2a_helper
, corresponding most closely to the code you shared:
static char* u2a_helper( const unsigned n,
char* const current,
const char* const lower_bound
)
/* Meant to be called only from u2a. Fills in the buffer from back to front,
* stopping if it reaches the start of the buffer (which should not happen).
*/
{
static const unsigned radix = 10;
const unsigned residue = n/radix;
*current = (char)('0' + n%radix);
if (residue > 0) {
assert( current > lower_bound );
MUSTTAIL return u2a_helper( residue, current-1, lower_bound );
}
return current;
}
GCC 11.2 does an excellent job with this program. (I slightly modified it to compile on Linux, with -std=c17 -m32 -Os -fomit-frame-pointer -foptimize-sibling-calls -march=x86-64-v3
). Even without support for the musttail
extension, to tell it that tail-call optimization is necessary, it was smart enough to perform it.
.LC1:
.string "current > lower_bound"
u2a_helper:
push edi
mov edi, 10
push esi
mov esi, ecx
mov ecx, edx
push ebx
mov ebx, eax
.L4:
mov eax, ebx
xor edx, edx
div edi
add edx, 48
mov BYTE PTR [ecx], dl
cmp ebx, 9
jbe .L1
cmp ecx, esi
ja .L3
push OFFSET FLAT:__PRETTY_FUNCTION__.1
push 76
push OFFSET FLAT:.LC0
push OFFSET FLAT:.LC1
call __assert_fail
.L3:
dec ecx
mov ebx, eax
jmp .L4
.L1:
pop ebx
mov eax, ecx
pop esi
pop edi
ret
Nearly all of this is preamble and the underflow check (which could be omitted in hot code, since we “know” the buffer is always big enough). The critical path compiles to a div
loop nine instructions long. This is essentially what you wanted, and any improvement you could get by hand-optimizing that further would be extremely marginal.
Next, clang 13.0.0. This is a great compiler to use on Windows, since it supports -target i386-windows-pc-msvc
, with variations for specific CPUs and versions of Visual C to emulate. Crucially, this target generates code compatible with Microsoft’s runtime, and other Windows libraries and headers. It can also target MingW32 and generate code compatible with GCC.
Clang sees that u2a_helper
is called only from u2a
and inlines it, so I’ll show u2a
.
.def _u2a;
.scl 3;
.type 32;
.endef
_u2a: # -- Begin function u2a
# @u2a
# %bb.0:
pushl %ebp
pushl %ebx
pushl %edi
pushl %esi
movl %edx, %esi
movl %ecx, %ebx
movb $0, 11(%edx)
movl $-858993459, %ebp # imm = 0xCCCCCCCD
movl %ecx, %edx
mulxl %ebp, %eax, %eax
leal 10(%esi), %edi
shrl $2, %eax
andl $-2, %eax
leal (%eax,%eax,4), %eax
subl %eax, %ecx
orb $48, %cl
movb %cl, 10(%esi)
cmpl $10, %ebx
jb LBB1_4
LBB1_1: # =>This Inner Loop Header: Depth=1
cmpl %esi, %edi
ja LBB1_3
# %bb.2: # in Loop: Header=BB1_1 Depth=1
pushl $27
pushl $"??_C@_1CG@MKELDBMD@?$AAm?$AAy?$AA_?$AAo?$AAu?$AAt?$AAp?$AAu?$AAt?$AA_?$AAh?$AAe?$AAl?$AAp?$AAe?$AAr?$AA?4?$AAc?$AA?$AA@"
pushl $"??_C@_1CM@MNODEPHG@?$AAc?$AAu?$AAr?$AAr?$AAe?$AAn?$AAt?$AA?5?$AA?$DO?$AA?5?$AAl?$AAo?$AAw?$AAe?$AAr?$AA_?$AAb?$AAo?$AAu?$AAn?$AAd?$AA?$AA@"
calll __wassert
addl $12, %esp
LBB1_3: # in Loop: Header=BB1_1 Depth=1
movl %ebx, %edx
mulxl %ebp, %edx, %edx
shrl $3, %edx
mulxl %ebp, %eax, %eax
shrl $2, %eax
andl $-2, %eax
leal (%eax,%eax,4), %eax
movl %edx, %ecx
subl %eax, %ecx
orb $48, %cl
movb %cl, -1(%edi)
decl %edi
cmpl $99, %ebx
movl %edx, %ebx
ja LBB1_1
LBB1_4:
movl %edi, %eax
popl %esi
popl %edi
popl %ebx
popl %ebp
retl
Clang transforms the tail-recursive /
and %
expressions into a mul
loop, even when told to optimize for space. It supports a musttail
extension, which I used heavily for this functional-style code, to force it to eliminate tail calls. Without the hint, it did not correctly compile the state machine. It’s my preferred compiler largely because of this.
ICX 2022.0.0 is based on Clang, and supports the same extensions, but only Intel CPUs as targets. It generates similar code, but can often optimize better. It therefore, unsurprisingly, also inlines u2a_helper
. In this case, I tested on Linux again.
u2a: #
push ebp
push ebx
push edi
push esi
sub esp, 12
mov esi, edx
mov byte ptr [edx + 11], 0
mov edi, -858993459
mov edx, ecx
mulx edx, edx, edi
lea eax, [esi + 10]
shr edx, 2
and edx, -2
lea edx, [edx + 4*edx]
mov ebx, ecx
sub ebx, edx
or bl, 48
mov byte ptr [esi + 10], bl
cmp ecx, 10
jb .LBB1_3
.LBB1_1: # =>This Inner Loop Header: Depth=1
cmp eax, esi
jbe .LBB1_4
mov edx, ecx
mulx ebp, ebp, edi
shr ebp, 3
mov edx, ebp
mulx ebx, ebx, edi
shr ebx, 2
and ebx, -2
lea ebx, [ebx + 4*ebx]
sub edx, ebx
or dl, 48
mov byte ptr [eax - 1], dl
dec eax
cmp ecx, 99
mov ecx, ebp
ja .LBB1_1
.LBB1_3:
add esp, 12
pop esi
pop edi
pop ebx
pop ebp
ret
.LBB1_4:
push offset .L__PRETTY_FUNCTION__.u2a_helper
push 76
push offset .L.str.3
push offset .L.str.2
call __assert_fail
The most interesting part of the code is .LBB1_1:
Here, ICX generates a slightly-different loop with two multiplications. This compiler seems to really want to optimize for speed, even when I tell it to optimize for space with -Os
.
Finally, I tried this on MSVC 19.30.30706 for x86, with the command line
cl /utf-8 /std:c17 /W4 /external:anglebrackets /external:W0 /FAu /Os /arch:AVX512 /c my_output_helper.c
MSVC has some serious problems with this code. It does not seem to be able to deal with recursion at all. When optimizing for space with /Os
, It does an okay job with the body of u2a_helper
, compiling it to a nice div
loop. However, it misses the tail-recursion optimization and generates a call u2a_helper
rather than a jmp
.
_TEXT SEGMENT
_residue$ = -4 ; size = 4
_n$ = 8 ; size = 4
_current$ = 12 ; size = 4
_lower_bound$ = 16 ; size = 4
_u2a_helper PROC
; File C:\Users\Loreh\Documents\Src\my_output_helper.c
; Line 20
push ebp
mov ebp, esp
push ecx
; Line 23
mov eax, DWORD PTR _n$[ebp]
xor edx, edx
div DWORD PTR ?radix@?1??u2a_helper@@9@9
mov DWORD PTR _residue$[ebp], eax
; Line 24
mov eax, DWORD PTR _n$[ebp]
xor edx, edx
div DWORD PTR ?radix@?1??u2a_helper@@9@9
add edx, 48 ; 00000030H
mov eax, DWORD PTR _current$[ebp]
mov BYTE PTR [eax], dl
; Line 26
cmp DWORD PTR _residue$[ebp], 0
jbe SHORT $LN2@u2a_helper
; Line 27
mov eax, DWORD PTR _current$[ebp]
cmp eax, DWORD PTR _lower_bound$[ebp]
ja SHORT $LN4@u2a_helper
push 27 ; 0000001bH
push OFFSET $SG7109
push OFFSET $SG7110
call __wassert
add esp, 12 ; 0000000cH
$LN4@u2a_helper:
; Line 28
push DWORD PTR _lower_bound$[ebp]
mov eax, DWORD PTR _current$[ebp]
dec eax
push eax
push DWORD PTR _residue$[ebp]
call _u2a_helper
add esp, 12 ; 0000000cH
jmp SHORT $LN1@u2a_helper
$LN2@u2a_helper:
; Line 31
mov eax, DWORD PTR _current$[ebp]
$LN1@u2a_helper:
; Line 32
leave
ret 0
_u2a_helper ENDP
_TEXT ENDS
END
When compiled with /O2
instead, to optimize for speed, it correctly converts the div
loop into a multiplication loop and also becomes able to optimize the tail-recursion in u2a_helper
. However, it is unable to optimize the state machine like the other three compilers all can, and therefore generates code that will cause a stack overflow on a long format string.
Therefore, with MSVC, you still need to write while
loops, not tail calls.
But if You Really, Truly Want to Do it That Way
See this answer on StackOverflow.
Again, the major problems are that you’re going to have to duplicate the entire code as soon as you want even a little flexibility (like being able to use fatal_error
to write formatted output to standard error instead of standard output), and that you’re using a fixed-sized buffer on the stack with no bounds checking. You do not want to optimize this code prematurely.