gdc or ldc for faster programs?

Ali Çehreli acehreli at yahoo.com
Wed Jan 26 18:00:41 UTC 2022


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



More information about the Digitalmars-d-learn mailing list