C#で、UnixTimeを作る

今回の結論

private static long _unixTimeMillisecond_baseTicks = new DateTime(1970, 1, 1).Ticks / TimeSpan.TicksPerMillisecond;

public long GetUnixTimeMillsecond() => 
    DateTime.UtcNow.Ticks / TimeSpan.TicksPerMillisecond - _unixTimeMillisecond_baseTicks;

今回のお話は、C#でUnixTimeを作る方法です。
システムの時間を拾って、longの値にします。
UnixTimeは、秒単位のものと、ミリ秒のものをよく見かけます。
今回は、ミリ秒を対象とします。

ネットでちょっと探してみると、以下の様なコードが見つかります。

long GetUnixTimeNow() => (long)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalMilliseconds;

long GetUnixTime2() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

実はこの2つ、概ね同じ値を返すのですが、ちょっと値が変わる時があります。
ちょっとテストコードで、試してみます。

[TestMethod()]
public void UnixTimeTest()
{
    var dt = new DateTime(638396640000009999, DateTimeKind.Utc);

    var a = (long)dt.Subtract(_baseTime).TotalMilliseconds; // 1704067200001
    var b = new DateTimeOffset(dt).ToUnixTimeMilliseconds();// 1704067200000

    Assert.IsTrue(a == b); // 一致しない
}

aの方で、TimeSpan.TotalMillsecondsを呼んでますが、これはdoubleを返します。
このため、境界値付近で、誤差が発生するんですね。
もちろん、Roundとかで補正することもできなくはないのですが、パフォーマンス面では不利になりますし、お勧めできません。

では、DateTimeOffset.ToUnixTimeMilliseconds でOKかという事になりますが、基本的に問題は無いですが、パフォーマンス面ではベストではありません。

それでは、以下はパフォーマンスについて検証を行っていきます。

public class UnixTimeBench
{
    private static DateTime _unixTime_BaseTime = new DateTime(1970, 1, 1);

    [Benchmark]
    public long A_DateTime_Now_TimeSpan_TotalMilliseconds() =>
        (long)(DateTime.Now.ToUniversalTime().Subtract(_unixTime_BaseTime).TotalMilliseconds);

    [Benchmark]
    public long B_DateTime_UtcNow_TimeSpan_TotalMilliseconds() => 
        (long)(DateTime.UtcNow.Subtract(_unixTime_BaseTime).TotalMilliseconds);

    [Benchmark]
    public long C_DateTimeOffset_UtcNow_ToUnixTimeMilliseconds() =>
        DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();


    private static long _unixTimeMillisecond_baseTicks = new DateTime(1970, 1, 1).Ticks / TimeSpan.TicksPerMillisecond;

    [Benchmark]
    public long D_DateTime_UtcNow_SelfCalc() => 
        DateTime.UtcNow.Ticks / TimeSpan.TicksPerMillisecond - _unixTimeMillisecond_baseTicks;
}

//| Method                                         | Mean     | Error    | StdDev   |
//|----------------------------------------------- |---------:|---------:|---------:|
//| A_DateTime_Now_TimeSpan_TotalMilliseconds      | 53.29 ns | 0.455 ns | 0.404 ns |
//| B_DateTime_UtcNow_TimeSpan_TotalMilliseconds   | 36.93 ns | 0.134 ns | 0.126 ns |
//| C_DateTimeOffset_UtcNow_ToUnixTimeMilliseconds | 34.54 ns | 0.134 ns | 0.119 ns |
//| D_DateTime_UtcNow_SelfCalc                     | 31.94 ns | 0.217 ns | 0.193 ns |

実行環境は、.Net8です。

では説明をしていきますが、速い方から行きます。
Dは、Cとほぼ同じ事をしています。Dは無駄が無く、最速です。
では、Cがなぜ若干遅いのかというと、いちどDateTimeOffsetにしている関係で、
内部で、2つのフィールドが作成されます。

private readonly DateTime _dateTime;
private readonly short _offsetMinutes;

今回のUnixTimeを求めるのに、UtcTimeのため、_offsetMinutesは不要です。その分遅くなっています。
A,Bは、上記の値の問題もありますのでお勧めしませんが、速度に関して分析します。
Bは、long型であるTicksをdoubleに変えて計算し、longに戻す処理があり、その分遅くなっています。
Aは、Bに加え、ローカルタイム(今回は日本時間)を取得し、UtcTimeに戻す処理が入ってるので、その分さらに遅くなってしまっています。

投稿日:
カテゴリー: C#

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です