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

Reply via email to