web-dev-qa-db-ja.com

Windowsで(マシン全体の)一意の64ビットタイムスタンプを生成する最速の方法

ここに私の問題のいくつかの特徴があります:

  • 一意 64ビットのタイムスタンププロセス全体を1台のマシンで生成する必要があります。私はそれらの64ビットに非常に制限されています。これは、ソリューションの一部としてGUIDを使用することを除外するようです。
  • 一意性は重要であり、(プロセス全体の)単調性はそれほど重要ではありません。
  • このコードが可能な限り高速であることが重要です(たとえば、いくつかのIPCメカニズムを通じて中央のタイムスタンプサービスを除外する可能性が高いです)。これはホットパスであることが証明されています。
  • プロセスの複数のインスタンスが同じシステムで実行されます。それぞれに1つのスレッドがこれらのIDを生成するタスクを割り当てられます。

私が現在実装している方法:

(プロセスがタイムスタンプを要求した場合)

  1. 初期タイムスタンプ値を取得します(利用可能な最速の基礎となるクロックソースから、たとえば、kernel32からGetSystemTimePreciseAsFileTimeを取得します)。
  2. グローバルミューテックスを取得する
  3. while(true) { ... }ループでは、値が「ロールオーバー」するまで、基になるクロックソースから値を取得し続けます(たとえば、(1)で取得した値から変更されます)
  4. グローバルミューテックスを解放します。

時間ソースが単調である限り、それは一意性の制約を満たすはずです。予想通り、グローバルミューテックスの取得と解放(カーネルコンテキストの切り替えなど)に関連する実際のコストはまだあります。

上記のように問題に使用できる他のアプローチまたはアルゴリズムはありますか?

5
ChristopheD

タイムスタンプを52ビットに圧縮してもよいとコメントで述べました。より小さなプロセスIDを交渉できると述べた人はほとんどいません。

マシン全体だけでなく、世界中でロックフリーの非常に高速なタイムスタンプを生成する1つの方法でこれらのアイデアを拡張しています。これは、プロセスが初期設定以外の状態を共有しないため、さまざまなマシンで簡単に実行できるためです。

次のように各タイムスタンプをエンコードします。

_ 8 bits -> shortPid, a generated "process id" during startup
52 bits -> shortTime, the compressed timestamp
 4 bits -> localCounter, to resolve timestamp overlaps, and avoid waiting for the system clock
_

アルゴリズム:

  1. 各プロセスが起動時に呼び出すshortPid- sを生成するための中心的な「サービス」を用意します。擬似コードの例:
_byte shortPid = 0;
byte getNewPid() {
    lock {
        shortPid++;
        return shortPid;
    }
}
_
  1. 各プロセスの開始時に、中央サービスからshortPidを取得します。
_ulong processPidMasked = getNewPid() << (52 + 4); // get pid and move into first 8 bits of 64-bit ulong
_
  1. 時間を効率的に生成するために、4ビットのlocalCounterを使用して、タイムティックごとに最大16(2 ^ 4)の一意の値を生成できるようにします。 52ビットのタイムスタンプを返す関数ulong getCompressedTimestamp()があるとすると、プロセスごとに一意のタイムスタンプを取得する方法は次のとおりです。
_ulong localCounter = 0;
ulong lastCompressedTimestamp = 0;
ulong getUniqueTimestamp() {
    while (1) {
        ulong now = getCompressedTimestamp(); // assuming 52-bit timestamp
        if (now != lastCompressedTimestamp) { // if we haven't generated any timestamp this tick
            lastCompressedTimestamp = now;
            localCounter = 0; // reset to time slot 0
            return processPidMasked | (now << 4) | localCounter;
        }

        // we generated one or more unique stamps on this tick, see if there's any "free slots" left
        if (localCounter < 15) { // if we have free slots for this tick
            localCounter++;
            return processPidMasked | (now << 4) | localCounter;
        }

        // worst case scenario: we exhausted our local counter for this tick:
        // loop until we get to the next time slot
    }
}
_

ノート:

  • 重要なコードはロックフリーであり、明示的な共有状態はありません。これにより、生成が非常に速くなります。唯一のロックは起動時です。
  • 圧縮されたタイムスタンプ、localCounter、およびshortProcessIdに割り当てるビット数を変更して、ユースケースに合わせることができます。現在のコードを使用すると、圧縮時間ティックごとに16のタイムスタンプを生成できます。
  • OS時間関数を呼び出すと、暗黙の共有状態になる可能性があります。あなたのシナリオでどのように機能するかわかりません。
  • Twitterは、Snowflakeと呼ばれるマシン全体で時間を並べ替え可能な一意のIDを生成するために、同様のアルゴリズムを使用(使用?)しています。マシンIDの一部、時間、および「シーケンス番号」の一部(ローカルカウンター)を格納します。ここでグーグルすることで見つけた同様の実装例 https://github.com/sony/sonyflake =
  • 上記のアルゴリズムを使用すると、理論的には、プロセスごとに1秒あたり最大1,600,000の一意のタイムスタンプを生成できます。うまくいけばあなたのケースには十分です:)
8
nokola

さて、最初にロックを解除します。
ロックレスにするほうがおそらくはるかに効率的です。

それでも不十分な場合は、バッチ処理を使用してください。
各ローカルスレッドに最大n個の連続するタイムスタンプを蓄積します。備蓄が枯渇したら、共有されたグローバルソースから補充してください。

不利な点は、需要が不規則または少ない場合に、タイムスタンプが実際の時間よりも大幅に遅れて快適になることです。

共有グローバルソースと各スレッドの間にプロセスレベルの備蓄を追加する可能性があります。
ホールドアップを回避するために、しきい値を下回ったときに非同期で備蓄を非同期で補充する可能性があります。

2
Deduplicator