正の10進整数の桁数を求める方法のいくつかと、その速度比較

以下の方法では0Int32.MaxValueの範囲の正の整数を扱う。 負数を扱うことは考えない。 負数に対して桁数を求める場合は事前に符合を逆転して正数として求めるなどする必要がある。

常用対数(log10)により求める

Math.Log10

少なくとも0Int32.MaxValueの範囲では正しい結果を返す。 (引数をInt64等に拡張した場合は、誤差により誤った結果を返す可能性がある)

数の桁数を求める・Math.Log10
  /// <summary>常用対数により数値<paramref name="n"/>の桁数を求める。 <see cref="Math.Log10(double)"/>を用いた実装。</summary>
  /// <exception cref="ArgumentOutOfRangeException"><paramref name="n"/>が負の数です。</exception>
  static int GetNumberOfDigits_Log10(int n)
  {
    if (n < 0)
      throw new ArgumentOutOfRangeException(nameof(n), n, "must be zero or positive number");

    return n == 0 ? 1 : 1 + unchecked((int)Math.Log10(n));
  }

MathF.Log10

MathF.Log10を使った場合では、0Int32.MaxValueの範囲内でも誤った結果を返す場合がある。

数の桁数を求める・MathF.Log10
  /// <summary>常用対数により数値<paramref name="n"/>の桁数を求める。 <see cref="MathF.Log10(float)"/>を用いた実装。</summary>
  /// <remarks>
  ///   <para>精度の問題により、<paramref name="n"/>が<c>9,999,999</c> <c>99,999,999</c> <c>999,999,999</c>以下付近の値のとき、誤った値を返す。</para>
  ///   <para><see cref="MathF.Log10(float)"/>は.NET Standard 2.1/.NET Core 2.0以降で利用可能。</para>
  /// </remarks>
  /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="n"/>が負の数です。</exception>
  static int GetNumberOfDigits_Log10F(int n)
  {
    if (n < 0)
      throw new ArgumentOutOfRangeException(nameof(n), n, "must be zero or positive number");

    return n == 0 ? 1 : 1 + unchecked((int)MathF.Log10(n));
  }

BigInteger.Log10

BigInteger.Log10を使った場合では、0Int32.MaxValueの範囲内でも誤った結果を返す場合がある。 誤りを許容できるなら、引数をInt64以上に拡張することができる。

数の桁数を求める・BigInteger.Log10
  /// <summary>常用対数により数値<paramref name="n"/>の桁数を求める。 <see cref="BigInteger.Log10(BigInteger)"/>を用いた実装。</summary>
  /// <remarks>
  ///   <para>精度の問題により、<paramref name="n"/>が<c>1,000</c> <c>1,000,000</c> <c>1,000,000,000</c>以上付近の値のとき、誤った値を返す。</para>
  /// </remarks>
  /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="n"/>が負の数です。</exception>
  static int GetNumberOfDigits_Log10BigInt(int n)
  {
    if (n < 0)
      throw new ArgumentOutOfRangeException(nameof(n), n, "must be zero or positive number");

    return n == 0 ? 1 : 1 + unchecked((int)BigInteger.Log10(n));
  }

文字列化したときの長さから求める

ToString

数の桁数を求める・ToString
  /// <summary>文字列化したときの長さより数値<paramref name="n"/>の桁数を求める。 <see cref="Int32.ToString(string, IFormatProvider)"/>を用いた実装。</summary>
  /// <remarks>
  ///   <para>Int32の規定の書式は"G"で、"G"はカルチャに依存せず桁区切り文字も含まれない形式となるので、書式もIFormatProviderも省略できる。</para>
  /// </remarks>
  /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="n"/>が負の数です。</exception>
  static int GetNumberOfDigits_LengthOfString_ToString(int n)
  {
    if (n < 0)
      throw new ArgumentOutOfRangeException(nameof(n), n, "must be zero or positive number");

    return n.ToString(null, null).Length;
  }

TryFormat + stackallock char[]

数の桁数を求める・TryFormat+stackallock char[]
  /// <summary>文字列化したときの長さより数値<paramref name="n"/>の桁数を求める。 <see cref="Int32.TryFormat(Span{char}, out int, ReadOnlySpan{char}, IFormatProvider)"/>を用いた実装。</summary>
  /// <remarks>
  ///   <para>Int32の規定の書式は"G"で、"G"はカルチャに依存せず桁区切り文字も含まれない形式となるので、書式もIFormatProviderも省略できる。</para>
  ///   <para><see cref="Int32.TryFormat(Span{char}, out int, ReadOnlySpan{char}, IFormatProvider)"/>は.NET Standard 2.1/.NET Core 2.1以降で利用可能。</para>
  /// </remarks>
  /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="n"/>が負の数です。</exception>
  static int GetNumberOfDigits_LengthOfString_TryFormat(int n)
  {
    if (n < 0)
      throw new ArgumentOutOfRangeException(nameof(n), n, "must be zero or positive number");

    Span<char> buffer = stackalloc char[10];

    if (n.TryFormat(buffer, out var charsWritten))
      return charsWritten;

    throw new NotImplementedException("never happen");
  }

TryFormat + static char[]

数の桁数を求める・TryFormat+static char[]
  /// <summary>文字列化したときの長さより数値<paramref name="n"/>の桁数を求める。 <see cref="Int32.TryFormat(Span{char}, out int, ReadOnlySpan{char}, IFormatProvider)"/>を用いた実装。</summary>
  /// <remarks>
  ///   <para>バッファを共有して使用するため、複数スレッドから同時に呼び出した場合の結果は保証されない。</para>
  ///   <para>Int32の規定の書式は"G"で、"G"はカルチャに依存せず桁区切り文字も含まれない形式となるので、書式もIFormatProviderも省略できる。</para>
  ///   <para><see cref="Int32.TryFormat(Span{char}, out int, ReadOnlySpan{char}, IFormatProvider)"/>は.NET Standard 2.1/.NET Core 2.1以降で利用可能。</para>
  /// </remarks>
  /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="n"/>が負の数です。</exception>
  static int GetNumberOfDigits_LengthOfString_TryFormatThreadUnsafe(int n)
  {
    if (n < 0)
      throw new ArgumentOutOfRangeException(nameof(n), n, "must be zero or positive number");

    if (n.TryFormat(tryFormatBuffer, out var charsWritten))
      return charsWritten;

    throw new NotImplementedException("never happen");
  }

  private static readonly char[] tryFormatBuffer = new char[10];

10で割れる回数から求める

数の桁数を求める・10で割れる回数から求める
  /// <summary>10で割り続けたときの回数により数値<paramref name="n"/>の桁数を求める。</summary>
  /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="n"/>が負の数です。</exception>
  static int GetNumberOfDigits_DivideByBaseNumber(int n)
  {
    if (n < 0)
      throw new ArgumentOutOfRangeException(nameof(n), n, "must be zero or positive number");

    for (var digit = 1; ; n /= 10, digit++) {
      if (n < 10)
        return digit;
    }

    throw new NotImplementedException("never happen");
  }

ある桁数での最大値と比較して求める

以下の方法ではいずれも桁数の小さい順に比較している。 与えられる数の大きさが均等に分布していると仮定した場合は、桁数の大きい順に比較したほうが効果的かもしれない。

事前に用意したテーブルと比較する

数の桁数を求める・事前に用意した桁数ごとの最大値と比較して求める
  /// <summary>事前に計算された桁数ごとの最大値と比較して<paramref name="n"/>の桁数を求める。</summary>
  /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="n"/>が負の数です。</exception>
  static int GetNumberOfDigits_CompareWithTable(int n)
  {
    if (n < 0)
      throw new ArgumentOutOfRangeException(nameof(n), n, "must be zero or positive number");

    for (var digit = 1; ; digit++) {
      if (n <= tableOfDigits[digit])
        return digit;
    }

    throw new NotImplementedException("never happen");
  }

  static readonly int[] tableOfDigits = {
                1 - 1,
               10 - 1,
              100 - 1,
            1_000 - 1,
           10_000 - 1,
          100_000 - 1,
        1_000_000 - 1,
       10_000_000 - 1,
      100_000_000 - 1,
    1_000_000_000 - 1,
    int.MaxValue // 10_000_000_000 - 1
  };

都度計算して比較する

数の桁数を求める・桁数ごとの最大値を都度計算・比較して求める
  /// <summary>各桁数での最大値を都度求めて比較することにより<paramref name="n"/>の桁数を求める。</summary>
  /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="n"/>が負の数です。</exception>
  static int GetNumberOfDigits_CompareWithRange(int n)
  {
    if (n < 0)
      throw new ArgumentOutOfRangeException(nameof(n), n, "must be zero or positive number");

    if (n == 0)
      return 1;

    var digit = 1;
    long max = 10L;

    for ( ; ; digit++, max *= 10L) {
      if (n < max)
        return digit;
    }

    throw new NotImplementedException("never happen");
  }

愚直に条件分岐して求める

数の桁数を求める・愚直に条件分岐して求める
  /// <summary>各桁数での最大値と比較することにより<paramref name="n"/>の桁数を求める。</summary>
  /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="n"/>が負の数です。</exception>
  static int GetNumberOfDigits_NaiveComparison(int n)
  {
    if (n <             0) throw new ArgumentOutOfRangeException(nameof(n), n, "must be zero or positive number");
    if (n <            10) return 1;
    if (n <           100) return 2;
    if (n <         1_000) return 3;
    if (n <        10_000) return 4;
    if (n <       100_000) return 5;
    if (n <     1_000_000) return 6;
    if (n <    10_000_000) return 7;
    if (n <   100_000_000) return 8;
    if (n < 1_000_000_000) return 9;

    return 10;
  }

速度比較

ランダムな数1,000,000個について、上記の各方法で桁数を求めた場合の速度を比較したもの。

値域が0〜10,000の場合
Method max Mean Error StdDev
Log10 10000 2.445 ms 0.0123 ms 0.0115 ms
Log10F 10000 2.505 ms 0.0285 ms 0.0293 ms
Log10BigInt 10000 2.754 ms 0.0244 ms 0.0228 ms
LengthOfString_ToString 10000 19.936 ms 0.0357 ms 0.0298 ms
LengthOfString_TryFormat 10000 35.177 ms 0.1110 ms 0.0867 ms
LengthOfString_TryFormatThreadUnsafe 10000 34.963 ms 0.2168 ms 0.3501 ms
DivideByBaseNumber 10000 2.587 ms 0.0089 ms 0.0084 ms
CompareWithTable 10000 3.958 ms 0.0179 ms 0.0150 ms
CompareWithRange 10000 2.223 ms 0.0053 ms 0.0044 ms
NaiveComparison 10000 2.440 ms 0.0095 ms 0.0079 ms
Method max Mean Error StdDev
値域が0〜Int32.MaxValueの場合
Method max Mean Error StdDev
Log10 2147483647 2.449 ms 0.0129 ms 0.0121 ms
Log10F 2147483647 2.440 ms 0.0074 ms 0.0062 ms
Log10BigInt 2147483647 2.758 ms 0.0238 ms 0.0223 ms
LengthOfString_ToString 2147483647 20.167 ms 0.1750 ms 0.1462 ms
LengthOfString_TryFormat 2147483647 35.219 ms 0.1215 ms 0.1077 ms
LengthOfString_TryFormatThreadUnsafe 2147483647 34.772 ms 0.0847 ms 0.0707 ms
DivideByBaseNumber 2147483647 2.451 ms 0.0101 ms 0.0085 ms
CompareWithTable 2147483647 4.001 ms 0.0243 ms 0.0216 ms
CompareWithRange 2147483647 2.228 ms 0.0100 ms 0.0084 ms
NaiveComparison 2147483647 2.448 ms 0.0102 ms 0.0085 ms
Method max Mean Error StdDev

検証に使った環境等
BenchmarkDotNet=v0.12.1, OS=ubuntu 20.04
Intel Core i5-6402P CPU 2.80GHz (Skylake), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=3.1.301
  [Host]     : .NET Core 3.1.5 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.27001), X64 RyuJIT
  DefaultJob : .NET Core 3.1.5 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.27001), X64 RyuJIT

検証に使ったコード
  public class RandomNumberBenchmark {
    [Params(10_000, int.MaxValue)]
    public int max = 0;

    private const int count = 1_000_000;
    private readonly int[] randomNumbers;

    public RandomNumberBenchmark()
    {
      var rand = new Random(Seed: 42);

      randomNumbers = new int[count];

      for (var i = 0; i < count; i++) {
        randomNumbers[i] = rand.Next(0, max);
      }
    }

    [Benchmark] public int Log10()
      { var ret = 0; for (var i = 0; i < count; i++) ret += GetNumberOfDigits_Log10(randomNumbers[i]); return ret; }
    [Benchmark] public int Log10F()
      { var ret = 0; for (var i = 0; i < count; i++) ret += GetNumberOfDigits_Log10F(randomNumbers[i]); return ret; }
    [Benchmark] public int Log10BigInt()
      { var ret = 0; for (var i = 0; i < count; i++) ret += GetNumberOfDigits_Log10BigInt(randomNumbers[i]); return ret; }
    [Benchmark] public int LengthOfString_ToString()
      { var ret = 0; for (var i = 0; i < count; i++) ret += GetNumberOfDigits_LengthOfString_ToString(randomNumbers[i]); return ret; }
    [Benchmark] public int LengthOfString_TryFormat()
      { var ret = 0; for (var i = 0; i < count; i++) ret += GetNumberOfDigits_LengthOfString_TryFormat(randomNumbers[i]); return ret; }
    [Benchmark] public int LengthOfString_TryFormatThreadUnsafe()
      { var ret = 0; for (var i = 0; i < count; i++) ret += GetNumberOfDigits_LengthOfString_TryFormatThreadUnsafe(randomNumbers[i]); return ret; }
    [Benchmark] public int DivideByBaseNumber()
      { var ret = 0; for (var i = 0; i < count; i++) ret += GetNumberOfDigits_DivideByBaseNumber(randomNumbers[i]); return ret; }
    [Benchmark] public int CompareWithTable()
      { var ret = 0; for (var i = 0; i < count; i++) ret += GetNumberOfDigits_CompareWithTable(randomNumbers[i]); return ret; }
    [Benchmark] public int CompareWithRange()
      { var ret = 0; for (var i = 0; i < count; i++) ret += GetNumberOfDigits_CompareWithRange(randomNumbers[i]); return ret; }
    [Benchmark] public int NaiveComparison()
      { var ret = 0; for (var i = 0; i < count; i++) ret += GetNumberOfDigits_NaiveComparison(randomNumbers[i]); return ret; }
  }

コード全文

GetNumberOfDigits.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
  </ItemGroup>

</Project>
GetNumberOfDigits.cs
using System;
using System.Numerics;
using System.Runtime.InteropServices;

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

public class GetNumberOfDigits {
  /// <summary>常用対数により数値<paramref name="n"/>の桁数を求める。 <see cref="Math.Log10(double)"/>を用いた実装。</summary>
  /// <exception cref="ArgumentOutOfRangeException"><paramref name="n"/>が負の数です。</exception>
  static int GetNumberOfDigits_Log10(int n)
  {
    if (n < 0)
      throw new ArgumentOutOfRangeException(nameof(n), n, "must be zero or positive number");

    return n == 0 ? 1 : 1 + unchecked((int)Math.Log10(n));
  }

  /// <summary>常用対数により数値<paramref name="n"/>の桁数を求める。 <see cref="MathF.Log10(float)"/>を用いた実装。</summary>
  /// <remarks>
  ///   <para>精度の問題により、<paramref name="n"/>が<c>9,999,999</c> <c>99,999,999</c> <c>999,999,999</c>以下付近の値のとき、誤った値を返す。</para>
  ///   <para><see cref="MathF.Log10(float)"/>は.NET Standard 2.1/.NET Core 2.0以降で利用可能。</para>
  /// </remarks>
  /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="n"/>が負の数です。</exception>
  static int GetNumberOfDigits_Log10F(int n)
  {
    if (n < 0)
      throw new ArgumentOutOfRangeException(nameof(n), n, "must be zero or positive number");

    return n == 0 ? 1 : 1 + unchecked((int)MathF.Log10(n));
  }

  /// <summary>常用対数により数値<paramref name="n"/>の桁数を求める。 <see cref="BigInteger.Log10(BigInteger)"/>を用いた実装。</summary>
  /// <remarks>
  ///   <para>精度の問題により、<paramref name="n"/>が<c>1,000</c> <c>1,000,000</c> <c>1,000,000,000</c>以上付近の値のとき、誤った値を返す。</para>
  /// </remarks>
  /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="n"/>が負の数です。</exception>
  static int GetNumberOfDigits_Log10BigInt(int n)
  {
    if (n < 0)
      throw new ArgumentOutOfRangeException(nameof(n), n, "must be zero or positive number");

    return n == 0 ? 1 : 1 + unchecked((int)BigInteger.Log10(n));
  }

  /// <summary>文字列化したときの長さより数値<paramref name="n"/>の桁数を求める。 <see cref="Int32.ToString(string, IFormatProvider)"/>を用いた実装。</summary>
  /// <remarks>
  ///   <para>Int32の規定の書式は"G"で、"G"はカルチャに依存せず桁区切り文字も含まれない形式となるので、書式もIFormatProviderも省略できる。</para>
  /// </remarks>
  /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="n"/>が負の数です。</exception>
  static int GetNumberOfDigits_LengthOfString_ToString(int n)
  {
    if (n < 0)
      throw new ArgumentOutOfRangeException(nameof(n), n, "must be zero or positive number");

    return n.ToString(null, null).Length;
  }

  /// <summary>文字列化したときの長さより数値<paramref name="n"/>の桁数を求める。 <see cref="Int32.TryFormat(Span{char}, out int, ReadOnlySpan{char}, IFormatProvider)"/>を用いた実装。</summary>
  /// <remarks>
  ///   <para>Int32の規定の書式は"G"で、"G"はカルチャに依存せず桁区切り文字も含まれない形式となるので、書式もIFormatProviderも省略できる。</para>
  ///   <para><see cref="Int32.TryFormat(Span{char}, out int, ReadOnlySpan{char}, IFormatProvider)"/>は.NET Standard 2.1/.NET Core 2.1以降で利用可能。</para>
  /// </remarks>
  /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="n"/>が負の数です。</exception>
  static int GetNumberOfDigits_LengthOfString_TryFormat(int n)
  {
    if (n < 0)
      throw new ArgumentOutOfRangeException(nameof(n), n, "must be zero or positive number");

    Span<char> buffer = stackalloc char[10];

    if (n.TryFormat(buffer, out var charsWritten))
      return charsWritten;

    throw new NotImplementedException("never happen");
  }

  /// <summary>文字列化したときの長さより数値<paramref name="n"/>の桁数を求める。 <see cref="Int32.TryFormat(Span{char}, out int, ReadOnlySpan{char}, IFormatProvider)"/>を用いた実装。</summary>
  /// <remarks>
  ///   <para>バッファを共有して使用するため、複数スレッドから同時に呼び出した場合の結果は保証されない。</para>
  ///   <para>Int32の規定の書式は"G"で、"G"はカルチャに依存せず桁区切り文字も含まれない形式となるので、書式もIFormatProviderも省略できる。</para>
  ///   <para><see cref="Int32.TryFormat(Span{char}, out int, ReadOnlySpan{char}, IFormatProvider)"/>は.NET Standard 2.1/.NET Core 2.1以降で利用可能。</para>
  /// </remarks>
  /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="n"/>が負の数です。</exception>
  static int GetNumberOfDigits_LengthOfString_TryFormatThreadUnsafe(int n)
  {
    if (n < 0)
      throw new ArgumentOutOfRangeException(nameof(n), n, "must be zero or positive number");

    if (n.TryFormat(tryFormatBuffer, out var charsWritten))
      return charsWritten;

    throw new NotImplementedException("never happen");
  }

  private static readonly char[] tryFormatBuffer = new char[10];

  /// <summary>10で割り続けたときの回数により数値<paramref name="n"/>の桁数を求める。</summary>
  /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="n"/>が負の数です。</exception>
  static int GetNumberOfDigits_DivideByBaseNumber(int n)
  {
    if (n < 0)
      throw new ArgumentOutOfRangeException(nameof(n), n, "must be zero or positive number");

    for (var digit = 1; ; n /= 10, digit++) {
      if (n < 10)
        return digit;
    }

    throw new NotImplementedException("never happen");
  }

  /// <summary>事前に計算された桁数ごとの最大値と比較して<paramref name="n"/>の桁数を求める。</summary>
  /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="n"/>が負の数です。</exception>
  static int GetNumberOfDigits_CompareWithTable(int n)
  {
    if (n < 0)
      throw new ArgumentOutOfRangeException(nameof(n), n, "must be zero or positive number");

    for (var digit = 1; ; digit++) {
      if (n <= tableOfDigits[digit])
        return digit;
    }

    throw new NotImplementedException("never happen");
  }

  static readonly int[] tableOfDigits = {
                1 - 1,
               10 - 1,
              100 - 1,
            1_000 - 1,
           10_000 - 1,
          100_000 - 1,
        1_000_000 - 1,
       10_000_000 - 1,
      100_000_000 - 1,
    1_000_000_000 - 1,
    int.MaxValue // 10_000_000_000 - 1
  };

  /// <summary>各桁数での最大値を都度求めて比較することにより<paramref name="n"/>の桁数を求める。</summary>
  /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="n"/>が負の数です。</exception>
  static int GetNumberOfDigits_CompareWithRange(int n)
  {
    if (n < 0)
      throw new ArgumentOutOfRangeException(nameof(n), n, "must be zero or positive number");

    if (n == 0)
      return 1;

    var digit = 1;
    long max = 10L;

    for ( ; ; digit++, max *= 10L) {
      if (n < max)
        return digit;
    }

    throw new NotImplementedException("never happen");
  }

  /// <summary>各桁数での最大値と比較することにより<paramref name="n"/>の桁数を求める。</summary>
  /// <exception cref="T:System.ArgumentOutOfRangeException"><paramref name="n"/>が負の数です。</exception>
  static int GetNumberOfDigits_NaiveComparison(int n)
  {
    if (n <             0) throw new ArgumentOutOfRangeException(nameof(n), n, "must be zero or positive number");
    if (n <            10) return 1;
    if (n <           100) return 2;
    if (n <         1_000) return 3;
    if (n <        10_000) return 4;
    if (n <       100_000) return 5;
    if (n <     1_000_000) return 6;
    if (n <    10_000_000) return 7;
    if (n <   100_000_000) return 8;
    if (n < 1_000_000_000) return 9;

    return 10;
  }

  static void TestLog10()
  {
    foreach (var n in new[] {
                  1,
                 10,
                100,
              1_000,
             10_000,
            100_000,
          1_000_000,
         10_000_000,
        100_000_000,
      1_000_000_000,
    }) {
      Console.WriteLine($"[n = {n}]");

      Console.WriteLine($"GetNumberOfDigits(n - 1)             = {GetNumberOfDigits_LengthOfString_ToString(n - 1)} (expected result)");
      Console.WriteLine($"{nameof(GetNumberOfDigits_Log10)}(n - 1)       = {GetNumberOfDigits_Log10(n - 1)}");
      Console.WriteLine($"{nameof(GetNumberOfDigits_Log10F)}(n - 1)      = {GetNumberOfDigits_Log10F(n - 1)}");
      Console.WriteLine($"{nameof(GetNumberOfDigits_Log10BigInt)}(n - 1) = {GetNumberOfDigits_Log10BigInt(n - 1)}");
      Console.WriteLine();

      Console.WriteLine($"GetNumberOfDigits(n)                 = {GetNumberOfDigits_LengthOfString_ToString(n)} (expected result)");
      Console.WriteLine($"{nameof(GetNumberOfDigits_Log10)}(n)           = {GetNumberOfDigits_Log10(n)}");
      Console.WriteLine($"{nameof(GetNumberOfDigits_Log10F)}(n)          = {GetNumberOfDigits_Log10F(n)}");
      Console.WriteLine($"{nameof(GetNumberOfDigits_Log10BigInt)}(n)     = {GetNumberOfDigits_Log10BigInt(n)}");
      Console.WriteLine();

      Console.WriteLine($"GetNumberOfDigits(n + 1)             = {GetNumberOfDigits_LengthOfString_ToString(n + 1)} (expected result)");
      Console.WriteLine($"{nameof(GetNumberOfDigits_Log10)}(n + 1)       = {GetNumberOfDigits_Log10(n + 1)}");
      Console.WriteLine($"{nameof(GetNumberOfDigits_Log10F)}(n + 1)      = {GetNumberOfDigits_Log10F(n + 1)}");
      Console.WriteLine($"{nameof(GetNumberOfDigits_Log10BigInt)}(n + 1) = {GetNumberOfDigits_Log10BigInt(n + 1)}");
      Console.WriteLine();
    }
  }

  public class RandomNumberBenchmark {
    [Params(10_000, int.MaxValue)]
    public int max = 0;

    private const int count = 1_000_000;
    private readonly int[] randomNumbers;

    public RandomNumberBenchmark()
    {
      var rand = new Random(Seed: 42);

      randomNumbers = new int[count];

      for (var i = 0; i < count; i++) {
        randomNumbers[i] = rand.Next(0, max);
      }
    }

    [Benchmark] public int Log10()
      { var ret = 0; for (var i = 0; i < count; i++) ret += GetNumberOfDigits_Log10(randomNumbers[i]); return ret; }
    [Benchmark] public int Log10F()
      { var ret = 0; for (var i = 0; i < count; i++) ret += GetNumberOfDigits_Log10F(randomNumbers[i]); return ret; }
    [Benchmark] public int Log10BigInt()
      { var ret = 0; for (var i = 0; i < count; i++) ret += GetNumberOfDigits_Log10BigInt(randomNumbers[i]); return ret; }
    [Benchmark] public int LengthOfString_ToString()
      { var ret = 0; for (var i = 0; i < count; i++) ret += GetNumberOfDigits_LengthOfString_ToString(randomNumbers[i]); return ret; }
    [Benchmark] public int LengthOfString_TryFormat()
      { var ret = 0; for (var i = 0; i < count; i++) ret += GetNumberOfDigits_LengthOfString_TryFormat(randomNumbers[i]); return ret; }
    [Benchmark] public int LengthOfString_TryFormatThreadUnsafe()
      { var ret = 0; for (var i = 0; i < count; i++) ret += GetNumberOfDigits_LengthOfString_TryFormatThreadUnsafe(randomNumbers[i]); return ret; }
    [Benchmark] public int DivideByBaseNumber()
      { var ret = 0; for (var i = 0; i < count; i++) ret += GetNumberOfDigits_DivideByBaseNumber(randomNumbers[i]); return ret; }
    [Benchmark] public int CompareWithTable()
      { var ret = 0; for (var i = 0; i < count; i++) ret += GetNumberOfDigits_CompareWithTable(randomNumbers[i]); return ret; }
    [Benchmark] public int CompareWithRange()
      { var ret = 0; for (var i = 0; i < count; i++) ret += GetNumberOfDigits_CompareWithRange(randomNumbers[i]); return ret; }
    [Benchmark] public int NaiveComparison()
      { var ret = 0; for (var i = 0; i < count; i++) ret += GetNumberOfDigits_NaiveComparison(randomNumbers[i]); return ret; }
  }

  static void Main()
  {
#if false
    foreach (var num in new[] {0, 1, 9, 10, 11, 99, 100, 500, 999, 1000, 6789, 10000, 99999, 1_000_000_000, 1_000_000_001, int.MaxValue - 1, int.MaxValue}) {
      Console.WriteLine("[{0}]", num);
      Console.WriteLine(GetNumberOfDigits_Log10(num));
      Console.WriteLine(GetNumberOfDigits_Log10F(num));
      Console.WriteLine(GetNumberOfDigits_Log10BigInt(num));
      Console.WriteLine(GetNumberOfDigits_LengthOfString_ToString(num));
      Console.WriteLine(GetNumberOfDigits_LengthOfString_TryFormat(num));
      Console.WriteLine(GetNumberOfDigits_LengthOfString_TryFormatThreadUnsafe(num));
      Console.WriteLine(GetNumberOfDigits_DivideByBaseNumber(num));
      Console.WriteLine(GetNumberOfDigits_CompareWithTable(num));
      Console.WriteLine(GetNumberOfDigits_CompareWithRange(num));
      Console.WriteLine(GetNumberOfDigits_NaiveComparison(num));
      Console.WriteLine();
    }
#endif

#if false
    TestLog10();
    return;
#endif

    BenchmarkRunner.Run<RandomNumberBenchmark>();
  }
}

以前の検証 (Mono)

ランダムな数10,000,000個について桁数を求めた場合の速度を比較したもの。 それぞれ3回ずつ試行。 Ubuntu 11.10 + Mono 2.10.8での実行結果。

  • 結果
    • 最速はテーブルを使った方法、最も遅いのは文字列化
    • 10で割るよりはMath.Log10を使った方が早い
    • ただし、数がある程度小さい場合は、Math.Log10より10で割る方が速くなる
    • Math.Log10の速度は数の大きさによらず一定
数の範囲が1 ~ int.MaxValueの場合
4.0.30319.1
Unix 3.0.0.14
1 ~ 2147483647

Math.Log10    : 00:00:01.1368336
String.Length : 00:00:04.1699058
divide by 10  : 00:00:01.7512138
table         : 00:00:00.7284158

Math.Log10    : 00:00:01.1065396
String.Length : 00:00:04.1047607
divide by 10  : 00:00:01.7776515
table         : 00:00:00.7468283

Math.Log10    : 00:00:01.1293331
String.Length : 00:00:04.1484966
divide by 10  : 00:00:01.7761593
table         : 00:00:00.7391820
数の範囲が1 ~ 10000の場合
4.0.30319.1
Unix 3.0.0.14
1 ~ 10000

Math.Log10    : 00:00:01.1085428
String.Length : 00:00:02.9993363
divide by 10  : 00:00:00.7884117
table         : 00:00:00.5584225

Math.Log10    : 00:00:01.1094404
String.Length : 00:00:02.9639001
divide by 10  : 00:00:00.7818080
table         : 00:00:00.5582405

Math.Log10    : 00:00:01.1111837
String.Length : 00:00:02.9534707
divide by 10  : 00:00:00.8002473
table         : 00:00:00.5687480
検証に使ったコード
using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;

class Sample {
  static readonly int[] digits = {0, 9, 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999, int.MaxValue};

  const int min = 1;
  const int max = int.MaxValue;

  static IEnumerable<int> CreateSource(int seed, int count)
  {
    var rand = new Random(seed);

    for (; 0 < count; count--) {
      yield return rand.Next(min, max);
    }
  }

  static void Main()
  {
    Console.WriteLine(Environment.Version);
    Console.WriteLine(Environment.OSVersion);
    Console.WriteLine("{0} ~ {1}", min, max);
    Console.WriteLine();

    const int count = 10 * 1000 * 1000;
    int digit = 0;

    for (var i = 0; i < 3; i++) {
      var seed = unchecked((int)Environment.TickCount);

      var sw1 = Stopwatch.StartNew();

      foreach (var num in CreateSource(seed, count)) {
        digit = (int)Math.Log10(num) + 1;
      }

      sw1.Stop();

      Console.WriteLine("Math.Log10    : {0}", sw1.Elapsed);

      var sw2 = Stopwatch.StartNew();

      foreach (var num in CreateSource(seed, count)) {
        digit = num.ToString("D").Length;
      }

      sw2.Stop();

      Console.WriteLine("String.Length : {0}", sw2.Elapsed);

      var sw3 = Stopwatch.StartNew();

      foreach (var num in CreateSource(seed, count)) {
        digit = 1;
        for (var n = num; 10 <= n; digit++) {
          n /= 10;
        }
      }

      sw3.Stop();

      Console.WriteLine("divide by 10  : {0}", sw3.Elapsed);

      var sw4 = Stopwatch.StartNew();

      foreach (var num in CreateSource(seed, count)) {
        digit = 1;
        for (;;digit++) {
          if (num <= digits[digit])
            break;
        }
      }

      sw4.Stop();

      Console.WriteLine("table         : {0}", sw4.Elapsed);
      Console.WriteLine();
   }
  }
}