7

そのドキュメントによると、 System.nanoTime は 、固定されているが任意の起点 time からナノ秒を返します。ただし、以下のコードを試したすべての x64 マシンで、タイム ジャンプが発生し、固定された起点が移動しました。別の方法 (ここでは currentTimeMillis) を使用して正しい時刻を取得する方法には、何らかの欠陥がある可能性があります。ただし、相対時間 (期間) を測定するという主な目的にも悪影響があります。

さまざまなキューを LMAX の Disruptor と比較すると、レイテンシが非常にマイナスになることがあり、レイテンシを測定しようとしてこの問題に遭遇しました。このような場合、開始と終了のタイムスタンプは異なるスレッドによって作成されましたが、レイテンシはそれらのスレッドが終了した後に計算されました。

ここでの私のコードは、nanoTime を使用して時間がかかり、currentTimeMillis 時間で固定起点を計算し、呼び出し間でその起点を比較します。ここで質問しなければならないので、このコードの何が問題なのですか? 原産地固定契約の違反を観察するのはなぜですか? それともそうではありませんか?

import java.text.*;

/**
 * test coherency between {@link System#currentTimeMillis()} and {@link System#nanoTime()}
 */
public class TimeCoherencyTest {

    static final int MAX_THREADS = Math.max( 1, Runtime.getRuntime().availableProcessors() - 1);
    static final long RUNTIME_NS = 1000000000L * 100;
    static final long BIG_OFFSET_MS = 2;

    static long startNanos;
    static long firstNanoOrigin;
    static {
        initNanos();
    }

    private static void initNanos() {
        long    millisBefore = System.currentTimeMillis();
        long    millisAfter;
        do {
            startNanos = System.nanoTime();
            millisAfter = System.currentTimeMillis();
        } while ( millisAfter != millisBefore);
        firstNanoOrigin = ( long) ( millisAfter - ( startNanos / 1e6));
    }

    static NumberFormat lnf = DecimalFormat.getNumberInstance();
    static {
        lnf.setMaximumFractionDigits( 3);
        lnf.setGroupingUsed( true);
    };

    static class TimeCoherency {
        long    firstOrigin;
        long    lastOrigin;
        long    numMismatchToLast = 0;
        long    numMismatchToFirst = 0;
        long    numMismatchToFirstBig = 0;
        long    numChecks = 0;

        public TimeCoherency( long firstNanoOrigin) {
            firstOrigin = firstNanoOrigin;
            lastOrigin = firstOrigin;
        }
    }

    public static void main( String[] args) {
        Thread[]    threads = new Thread[ MAX_THREADS];
        for ( int i = 0;  i < MAX_THREADS;  i++) {
            final int   fi = i;
            final TimeCoherency tc = new TimeCoherency( firstNanoOrigin);
            threads[ i] = new Thread() {
                @Override
                public void run() {
                    long    start = getNow( tc);
                    long    firstOrigin = tc.lastOrigin;    // get the first origin for this thread
                    System.out.println( "Thread " + fi + " started at " + lnf.format( start) + " ns");
                    long    nruns = 0;
                    while ( getNow( tc) < RUNTIME_NS) {
                        nruns++;
                    }
                    final long  runTimeNS = getNow( tc) - start;
                    final long  originDrift = tc.lastOrigin - firstOrigin;
                    nruns += 3; // account for start and end call and the one that ends the loop
                    final long skipped = nruns - tc.numChecks;
                    System.out.println( "Thread " + fi + " finished after " + lnf.format( nruns) + " runs in " + lnf.format( runTimeNS) + " ns (" + lnf.format( ( double) runTimeNS / nruns) + " ns/call) with"
                            + "\n\t" + lnf.format( tc.numMismatchToFirst) + " different from first origin (" + lnf.format( 100.0 * tc.numMismatchToFirst / nruns) + "%)"
                            + "\n\t" + lnf.format( tc.numMismatchToLast) + " jumps from last origin (" + lnf.format( 100.0 * tc.numMismatchToLast / nruns) + "%)"
                            + "\n\t" + lnf.format( tc.numMismatchToFirstBig) + " different from first origin by more than " + BIG_OFFSET_MS + " ms"
                                    + " (" + lnf.format( 100.0 * tc.numMismatchToFirstBig / nruns) + "%)"
                            + "\n\t" + "total drift: " + lnf.format( originDrift) + " ms, " + lnf.format( skipped) + " skipped (" + lnf.format( 100.0 * skipped / nruns) + " %)");
                }};
            threads[ i].start();
        }
        try {
            for ( Thread thread : threads) {
                thread.join();
            }
        } catch ( InterruptedException ie) {};
    }

    public static long getNow( TimeCoherency coherency) {
        long    millisBefore = System.currentTimeMillis();
        long    now = System.nanoTime();
        if ( coherency != null) {
            checkOffset( now, millisBefore, coherency);
        }
        return now - startNanos;
    }

    private static void checkOffset( long nanoTime, long millisBefore, TimeCoherency tc) {
        long    millisAfter = System.currentTimeMillis();
        if ( millisBefore != millisAfter) {
            // disregard since thread may have slept between calls
            return;
        }
        tc.numChecks++;
        long    nanoMillis = ( long) ( nanoTime / 1e6);
        long    nanoOrigin = millisAfter - nanoMillis;
        long    oldOrigin = tc.lastOrigin;
        if ( oldOrigin != nanoOrigin) {
            tc.lastOrigin = nanoOrigin;
            tc.numMismatchToLast++;
        }
        if ( tc.firstOrigin != nanoOrigin) {
            tc.numMismatchToFirst++;
        }
        if ( Math.abs( tc.firstOrigin - nanoOrigin) > BIG_OFFSET_MS) {
            tc.numMismatchToFirstBig ++;
        }
    }
}

今、私はいくつかの小さな変更を加えました。基本的に、nanoTime 呼び出しを 2 つの currentTimeMillis 呼び出しの間にまとめて、スレッドが再スケジュールされているかどうかを確認します (currentTimeMillis の解像度よりも長くかかるはずです)。この場合、ループ サイクルは無視します。実際、nanoTime が十分に高速であることがわかっている場合 (Ivy Bridge などの新しいアーキテクチャのように)、currentTimeMillis を nanoTime で囲むことができます。

これで、10 ミリ秒を超える長いジャンプはなくなりました。代わりに、スレッドごとに最初のオリジンから 2 ミリ秒以上離れたときにカウントします。私がテストしたマシンでは、100 秒の実行時間で、呼び出しの間に常に 200.000 近くのジャンプがあります。currentTimeMillis または nanoTime のいずれかが不正確であると思われるのは、このような場合です。

4

2 に答える 2

4

前述のように、毎回新しい原点を計算すると、エラーが発生する可能性があります。

//                               ______ delay _______
//                              v                    v
long origin = (long)(System.currentTimeMillis() - System.nanoTime() / 1e6);
//                                                                  ^
//                                                            truncation

原点の差も計算するようにプログラムを変更すると、それが非常に小さいことがわかります。私が測定した平均値は約 200ns で、これは遅延時間としてはほぼ正しい値です。

除算の代わりに乗算を使用すると (さらに数百年オーバーフローすることなく問題ないはずです)、等値チェックに失敗する計算された原点の数がはるかに多く、約 99% になることもわかります。エラーの理由が時間遅延によるものである場合、遅延がたまたま最後の遅延と同じである場合にのみ合格となります。

はるかに簡単なテストは、nanoTime へのいくつかの後続の呼び出しで経過時間を累積し、最初と最後の呼び出しでチェックアウトするかどうかを確認することです。

public class SimpleTimeCoherencyTest {
    public static void main(String[] args) {
        final long anchorNanos = System.nanoTime();

        long lastNanoTime = System.nanoTime();
        long accumulatedNanos = lastNanoTime - anchorNanos;

        long numCallsSinceAnchor = 1L;

        for(int i = 0; i < 100; i++) {
            TestRun testRun = new TestRun(accumulatedNanos, lastNanoTime);

            Thread t = new Thread(testRun);
            t.start();

            try {
                t.join();
            } catch(InterruptedException ie) {}

            lastNanoTime = testRun.lastNanoTime;
            accumulatedNanos = testRun.accumulatedNanos;
            numCallsSinceAnchor += testRun.numCallsToNanoTime;
        }

        System.out.println(numCallsSinceAnchor);
        System.out.println(accumulatedNanos);
        System.out.println(lastNanoTime - anchorNanos);
    }

    static class TestRun
    implements Runnable {
        volatile long accumulatedNanos;
        volatile long lastNanoTime;
        volatile long numCallsToNanoTime;

        TestRun(long acc, long last) {
            accumulatedNanos = acc;
            lastNanoTime = last;
        }

        @Override
        public void run() {
            long lastNanos = lastNanoTime;
            long currentNanos;

            do {
                currentNanos = System.nanoTime();
                accumulatedNanos += currentNanos - lastNanos;
                lastNanos = currentNanos;
                numCallsToNanoTime++;
            } while(currentNanos - lastNanoTime <= 100000000L);

            lastNanoTime = lastNanos;
        }
    }
}

そのテストは、原点が同じであることを示しています (または、少なくともエラーはゼロ平均です)。

于 2014-01-02T01:22:35.413 に答える