ldc shines with sprintf. And dmd suprises by being a little bit faster
than gdc! (?)
ldc (2.098.0): ~6.2 seconds
dmd (2.098.1): ~7.4 seconds
gdc (2.076.?): ~7.5 seconds
Again, here are the versions of the compilers that are readily available
on my system:
> ldc: LDC - the LLVM D compiler (1.28.0):
> based on DMD v2.098.0 and LLVM 13.0.0
>
> gdc: dc (GCC) 11.1.0 (Uses dmd 2.076 front end)
>
> dmd: DMD64 D Compiler v2.098.1
They were compiled with
dub run --compiler=<COMPILER> --build=release-nobounds --verbose
where <COMPILER> was ldc, dmd, or gdc.
I replaced formattedWrite in the code with sprintf. For example, the
inner loop became
foreach (divider; dividers!T.retro) {
const quotient = number / divider.value;
if (quotient) {
output += sprintf(output, fmt!T.ptr, quotient, divider.word.ptr);
}
number %= divider.value;
}
}
For completeness (and noise :/) here is the final version of the program:
module spellout.spellout;
// This program was written as a programming kata to spell out
// certain parts of integers as in "1 million 2 thousand
// 42". Note that this way of spelling-out numbers is not
// grammatically correct in English.
// Returns a string that contains the partly spelled-out version
// of the parameter.
//
// You must copy the returned string when needed as this function
// uses the same internal buffer for all invocations of the same
// template instance.
auto spellOut(T)(in T number_) {
import std.string : strip;
import std.traits : Unqual;
import std.meta : AliasSeq;
import core.stdc.stdio : sprintf;
enum longestString =
"negative 9 quintillion 223 quadrillion 372 trillion" ~
" 36 billion 854 million 775 thousand 808";
static char[longestString.length + 1] buffer;
auto output = buffer.ptr;
// We treat these specially because the algorithm below does
// 'number = -number' and calls the same implementation
// function. The trouble is, for example, -int.min is still a
// negative number.
alias problematics = AliasSeq!(
byte, "negative 128",
short, "negative 32 thousand 768",
int, "negative 2 billion 147 million 483 thousand 648",
long, longestString);
static assert((problematics.length % 2) == 0);
static foreach (i, P; problematics) {
static if (i % 2) {
// This is a string; skip
} else {
// This is a problematic type
static if (is (T == P)) {
// Our T happens to be this problematic type
if (number_ == T.min) {
// and we are dealing with a problematic value
output += sprintf(output, problematics[i + 1].ptr);
return buffer[0 .. (output - buffer.ptr)];
}
}
}
}
auto number = cast(Unqual!T)number_; // Thanks 'in'! :p
if (number == 0) {
output += sprintf(output, "zero");
} else {
if (number < 0) {
output += sprintf(output, "negative");
static if (T.sizeof < int.sizeof) {
// Being careful with implicit conversions. (See the dmd
// command line switch -preview=intpromote)
number = cast(T)(-cast(int)number);
} else {
number = -number;
}
}
spellOutImpl(number, output);
}
return buffer[0 .. (output - buffer.ptr)].strip;
}
unittest {
assert(1_001_500.spellOut == "1 million 1 thousand 500");
assert((-1_001_500).spellOut ==
"negative 1 million 1 thousand 500");
assert(1_002_500.spellOut == "1 million 2 thousand 500");
}
template fmt(T) {
static if (is (T == long)||
is (T == ulong)) {
static fmt = " %lld %s";
} else {
static fmt = " %u %s";
}
}
import std.format : format;
void spellOutImpl(T)(T number, ref char * output)
in (number > 0, format!"Invalid number: %s"(number)) {
import std.range : retro;
import core.stdc.stdio : sprintf;
foreach (divider; dividers!T.retro) {
const quotient = number / divider.value;
if (quotient) {
output += sprintf(output, fmt!T.ptr, quotient, divider.word.ptr);
}
number %= divider.value;
}
}
struct Divider(T) {
T value; // 1_000, 1_000_000, etc.
string word; // "thousand", etc
}
// Returns the words related with the provided size of an
// integral type. The parameter is number of bytes
// e.g. int.sizeof
auto words(size_t typeSize) {
// This need not be recursive at all but it was fun using
// recursion.
final switch (typeSize) {
case 1: return [ "" ];
case 2: return words(1) ~ [ "thousand" ];
case 4: return words(2) ~ [ "million", "billion" ];
case 8: return words(4) ~ [ "trillion", "quadrillion", "quintillion" ];
}
}
unittest {
// These are relevant words for 'int' and 'uint' values:
assert(words(4) == [ "", "thousand", "million", "billion" ]);
}
// Returns a Divider!T array associated with T
auto dividers(T)() {
import std.range : array, enumerate;
import std.algorithm : map;
static const(Divider!T[]) result =
words(T.sizeof)
.enumerate!T
.map!(t => Divider!T(cast(T)(10^^(t.index * 3)), t.value))
.array;
return result;
}
unittest {
// Test a few entries
assert(dividers!int[1] == Divider!int(1_000, "thousand"));
assert(dividers!ulong[3] == Divider!ulong(1_000_000_000, "billion"));
}
void main() {
version (test) {
return;
}
import std.meta : AliasSeq;
import std.stdio : writefln;
import std.random : Random, uniform;
import std.conv : to;
static foreach (T; AliasSeq!(byte, ubyte, short, ushort,
int, uint, long, ulong)) {{
// A few numbers for each type
report(T.min);
report((T.max / 4).to!T); // Overcome int promotion for
// shorter types because I want
// to test with the exact type
// e.g. for byte.
report(T.max);
}}
enum count = 20_000_000;
writefln!"Testing with %,s random numbers"(spellOut(count));
// Use the same seed to be fair between compilations
enum seed = 0;
auto rnd = Random(seed);
ulong totalLength;
foreach (i; 0 .. count) {
const number = uniform(int.min, int.max, rnd);
const result = spellOut(number);
totalLength += result.length;
}
writefln!("A meaningless number to prevent the compiler from" ~
" removing the entire loop: %,s")(totalLength);
}
void report(T)(T number) {
import std.stdio : writefln;
writefln!" %6s % ,s: %s"(T.stringof, number, spellOut(number));
}
Ali