Just a silly test for msvcrt and ucrt sprintf.
pocrt: 3.141593
452 ticks
msvcrt: 3.141593
827 ticks
ucrt: 3.141593
749 ticks
test_sprintf3pocrt: 3.141593
439.653040
msvcrt: 3.141593
795.308200
ucrt: 3.141593
691.476080
thanks TimoVJL 👍
will give a good test Monday, I am off to visit my family and won't be back till Monday
@TimoVJL I am back early
trying to link against ucrt with Pelles C is not straightforward but it easy using llvm-mingw gcc from https://github.com/mstorsjo/llvm-mingw/releases
using the 64-bit version I compiled the following code: gcc sprintf-timing.c -o sprintf-timing
sprintf-timing.c
#include <stdio.h>
#include <windows.h>
#define ITERATIONS 1000000
int main() {
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);
char buffer[256];
LARGE_INTEGER frequency, start, end;
double x=1.2345678901234567890;
double total_time = 0;
QueryPerformanceFrequency(&frequency);
for (int i = 0; i < ITERATIONS; i++) {
QueryPerformanceCounter(&start);
sprintf(buffer, "%.15g", x);
QueryPerformanceCounter(&end);
total_time += (double)(end.QuadPart - start.QuadPart) / frequency.QuadPart;
}
printf("Average sprintf time: %.2f nanoseconds\n",
(total_time / ITERATIONS) * 1e9);
}
as you mentioned to Vortex, ucrtbase does not provide a straight printf or sprintf but it does provide stdio_common_vfprintf and stdio_common_vsprintf
I ran the following from the msys2 shell
objdump.exe -d -M intel,x86-64 sprintf-timing.exe >sprintf-timing.txt
and then searched the disassembly for stdio_common_vsprintf, here are the relevant pieces
0000000140001f30 <printf>:
140001f30: 56 push rsi
140001f31: 57 push rdi
140001f32: 48 83 ec 38 sub rsp,0x38
140001f36: 48 89 ce mov rsi,rcx
140001f39: 48 89 54 24 58 mov QWORD PTR [rsp+0x58],rdx
140001f3e: 4c 89 44 24 60 mov QWORD PTR [rsp+0x60],r8
140001f43: 4c 89 4c 24 68 mov QWORD PTR [rsp+0x68],r9
140001f48: 48 8d 44 24 58 lea rax,[rsp+0x58]
140001f4d: 48 89 44 24 30 mov QWORD PTR [rsp+0x30],rax
140001f52: e8 99 07 00 00 call 1400026f0 <__local_stdio_printf_options>
140001f57: 48 8b 38 mov rdi,QWORD PTR [rax]
140001f5a: b9 01 00 00 00 mov ecx,0x1
140001f5f: e8 ac 07 00 00 call 140002710 <__acrt_iob_func>
140001f64: 48 8b 4c 24 30 mov rcx,QWORD PTR [rsp+0x30]
140001f69: 48 89 4c 24 20 mov QWORD PTR [rsp+0x20],rcx
140001f6e: 48 89 f9 mov rcx,rdi
140001f71: 48 89 c2 mov rdx,rax
140001f74: 49 89 f0 mov r8,rsi
140001f77: 45 31 c9 xor r9d,r9d
140001f7a: e8 c1 09 00 00 call 140002940 <__stdio_common_vfprintf>
140001f7f: 90 nop
140001f80: 48 83 c4 38 add rsp,0x38
140001f84: 5f pop rdi
140001f85: 5e pop rsi
140001f86: c3 ret
140001f87: cc int3
140001f88: cc int3
140001f89: cc int3
140001f8a: cc int3
140001f8b: cc int3
140001f8c: cc int3
140001f8d: cc int3
140001f8e: cc int3
140001f8f: cc int3
0000000140001f90 <sprintf>:
140001f90: 56 push rsi
140001f91: 57 push rdi
140001f92: 48 83 ec 38 sub rsp,0x38
140001f96: 48 89 d6 mov rsi,rdx
140001f99: 48 89 cf mov rdi,rcx
140001f9c: 4c 89 44 24 60 mov QWORD PTR [rsp+0x60],r8
140001fa1: 4c 89 4c 24 68 mov QWORD PTR [rsp+0x68],r9
140001fa6: 48 8d 44 24 60 lea rax,[rsp+0x60]
140001fab: 48 89 44 24 30 mov QWORD PTR [rsp+0x30],rax
140001fb0: e8 3b 07 00 00 call 1400026f0 <__local_stdio_printf_options>
140001fb5: 48 8b 08 mov rcx,QWORD PTR [rax]
140001fb8: 48 83 c9 02 or rcx,0x2
140001fbc: 48 8b 44 24 30 mov rax,QWORD PTR [rsp+0x30]
140001fc1: 48 89 44 24 28 mov QWORD PTR [rsp+0x28],rax
140001fc6: 48 c7 44 24 20 00 00 mov QWORD PTR [rsp+0x20],0x0
140001fcd: 00 00
140001fcf: 48 89 fa mov rdx,rdi
140001fd2: 49 c7 c0 ff ff ff ff mov r8,0xffffffffffffffff
140001fd9: 49 89 f1 mov r9,rsi
140001fdc: e8 6f 09 00 00 call 140002950 <__stdio_common_vsprintf>
140001fe1: 90 nop
140001fe2: 48 83 c4 38 add rsp,0x38
140001fe6: 5f pop rdi
140001fe7: 5e pop rsi
140001fe8: c3 ret
140001fe9: cc int3
140001fea: cc int3
140001feb: cc int3
140001fec: cc int3
140001fed: cc int3
140001fee: cc int3
140001fef: cc int3
00000001400026f0 <__local_stdio_printf_options>:
1400026f0: 48 8d 05 51 29 00 00 lea rax,[rip+0x2951] # 140005048 <options>
1400026f7: c3 ret
1400026f8: cc int3
1400026f9: cc int3
1400026fa: cc int3
1400026fb: cc int3
1400026fc: cc int3
1400026fd: cc int3
1400026fe: cc int3
1400026ff: cc int3
the only missing piece of the puzzle is 140005048 <options>
A that <options> is a local storage ?
#if _CRT_FUNCTIONS_REQUIRED
// This function must not be inlined into callers to avoid ODR violations. The
// static local variable has different names in C and in C++ translation units.
_Check_return_ _Ret_notnull_
_CRT_INLINE_PURE_SECURITYCRITICAL_ATTRIBUTE
__declspec(noinline) __inline unsigned __int64* __CRTDECL __local_stdio_printf_options(void)
{
static unsigned __int64 _OptionsStorage;
return &_OptionsStorage;
}
// This function must not be inlined into callers to avoid ODR violations. The
// static local variable has different names in C and in C++ translation units.
_Check_return_ _Ret_notnull_
_CRT_INLINE_PURE_SECURITYCRITICAL_ATTRIBUTE
__declspec(noinline) __inline unsigned __int64* __CRTDECL __local_stdio_scanf_options(void)
{
static unsigned __int64 _OptionsStorage;
return &_OptionsStorage;
}
#endif
#if defined _M_CEE && !defined _M_CEE_PURE
#pragma managed(pop)
#endif
#define _CRT_INTERNAL_LOCAL_PRINTF_OPTIONS (*__local_stdio_printf_options())
#define _CRT_INTERNAL_LOCAL_SCANF_OPTIONS (*__local_stdio_scanf_options ())
#define _CRT_INTERNAL_PRINTF_LEGACY_VSPRINTF_NULL_TERMINATION (1ULL << 0)
#define _CRT_INTERNAL_PRINTF_STANDARD_SNPRINTF_BEHAVIOR (1ULL << 1)
#define _CRT_INTERNAL_PRINTF_LEGACY_WIDE_SPECIFIERS (1ULL << 2)
#define _CRT_INTERNAL_PRINTF_LEGACY_MSVCRT_COMPATIBILITY (1ULL << 3)
#define _CRT_INTERNAL_PRINTF_LEGACY_THREE_DIGIT_EXPONENTS (1ULL << 4)
#define _CRT_INTERNAL_PRINTF_STANDARD_ROUNDING (1ULL << 5)
#define _CRT_INTERNAL_SCANF_SECURECRT (1ULL << 0)
#define _CRT_INTERNAL_SCANF_LEGACY_WIDE_SPECIFIERS (1ULL << 1)
#define _CRT_INTERNAL_SCANF_LEGACY_MSVCRT_COMPATIBILITY (1ULL << 2)
// CRT headers are included into some kinds of source files where only data type
// definitions and macro definitions are required but function declarations and
// inline function definitions are not. These files include assembly files, IDL
// files, and resource files. The tools that process these files often have a
// limited ability to process C and C++ code. The _CRT_FUNCTIONS_REQUIRED macro
// is defined to 1 when we are compiling a file that actually needs functions to
// be declared (and defined, where applicable), and to 0 when we are compiling a
// file that does not. This allows us to suppress declarations and definitions
// that are not compilable with the aforementioned tools.
#if !defined _CRT_FUNCTIONS_REQUIRED
#if defined __assembler || defined __midl || defined RC_INVOKED
#define _CRT_FUNCTIONS_REQUIRED 0
#else
#define _CRT_FUNCTIONS_REQUIRED 1
#endif
#endif
Quote from: TimoVJL on February 08, 2026, 01:12:09 PMA that <options> is a local storage ?
that is my guess
with uppercase option D I get a bit more info
0000000140005048 <options>:
140005048: 24 00 and al,0x0
14000504a: 00 00 add BYTE PTR [rax],al
14000504c: 00 00 add BYTE PTR [rax],al
...
We can test this too ?
...
static unsigned __int64 _OptionsStorage; // local option storage
...
psprintf(((ULONGLONG)&_OptionsStorage|_CRT_INTERNAL_PRINTF_STANDARD_SNPRINTF_BEHAVIOR), buf, sizeof(buf), fmt, NULL, vl);
...
Hi Jack,
It's easy to build and use the import library for ucrt :
Import libraries for the Universal C Runtime :
https://forum.pellesc.de/index.php?topic=11052.0
Universal C Runtime example :
https://forum.pellesc.de/index.php?topic=11054.0
thanks Vortex
That silly test used only ucrtbase.dll, as those Api Stub forwanders mostly point to it.
Making ucrt.lib is more demanding, as have to collect those (stupid) forwanders are mostly used for versioning, and with no code.