スノーフレークアルゴリズムにおけるSystem.currentTimeMillis()の最適化は本当に有効か

スノーフレークアルゴリズムではID生成時にSystem.currentTimeMillis()を使用して現在時刻を取得します。このメソッドは毎回システムコールを発行するため、高並行环境下ではパフォーマンスに影響を与える可能性があるという意見があります。通常のオブジェクト生成よりも時間がかかる場合があるのは、Javaヒープ内での操作ではなく、ネイティブメソッド呼び出しが含まれるためです。

// 現在時刻をミリ秒単位で返す。戻り値の単位はミリ秒だが、
// 精度は基礎となるOSに依存し、より粗い場合がある
public static native long currentTimeMillis();

この課題に対し、バックグラウンドスレッドで定期的に時刻を更新する単一インスタンス時計を実装することで、システムコールを削減し性能向上を図るという最適化案が提唱されました。

この最適化は本当に効果があるのでしょうか。

最適化コードの実装例:

package timestamp;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

public class CachedTimer {
    
    private final int refreshInterval;
    private final AtomicLong cachedTime;
    private static final CachedTimer INSTANCE = new CachedTimer(1);
    
    private CachedTimer(int interval) {
        this.refreshInterval = interval;
        this.cachedTime = new AtomicLong(System.currentTimeMillis());
        initializePeriodicUpdate();
    }
    
    private void initializePeriodicUpdate() {
        ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(runnable -> {
            Thread thread = new Thread(runnable);
            thread.setDaemon(true);
            thread.setName("CachedTimer-Updater");
            return thread;
        });
        executor.scheduleAtFixedRate(() -> {
            cachedTime.set(System.currentTimeMillis());
        }, 0, refreshInterval, TimeUnit.MILLISECONDS);
    }
    
    private long fetchTime() {
        return cachedTime.get();
    }
    
    public static long currentTime() {
        return INSTANCE.fetchTime();
    }
}

スノーフレークアルゴリズムの実装例:

package idgenerator;

public class SnowflakeIdGenerator {
    
    private final long machineId;
    private final long datacenterId;
    private long sequence;
    
    public SnowflakeIdGenerator(long machineId, long datacenterId) {
        this(machineId, datacenterId, 0);
    }
    
    public SnowflakeIdGenerator(long machineId, long datacenterId, long initialSequence) {
        validateIds(machineId, datacenterId);
        this.machineId = machineId;
        this.datacenterId = datacenterId;
        this.sequence = initialSequence;
    }
    
    private final long epoch = 1634393012000L;
    private final long machineIdBits = 5L;
    private final long datacenterIdBits = 5L;
    private final long sequenceBits = 12L;
    
    private final long maxMachineId = -1L ^ (-1L << machineIdBits);
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);
    
    private final long machineIdShift = sequenceBits;
    private final long datacenterIdShift = sequenceBits + machineIdBits;
    private final long timestampShift = sequenceBits + machineIdBits + datacenterIdBits;
    
    private long lastTimestamp = -1L;
    
    private void validateIds(long machineId, long datacenterId) {
        if (machineId > maxMachineId || machineId < 0) {
            throw new IllegalArgumentException("Machine ID must be between 0 and " + maxMachineId);
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException("Datacenter ID must be between 0 and " + maxDatacenterId);
        }
    }
    
    public synchronized long generateId() {
        long currentTimestamp = getCurrentTimestamp();
        
        if (currentTimestamp < lastTimestamp) {
            throw new RuntimeException("Clock moved backwards. Refusing to generate ID.");
        }
        
        if (currentTimestamp == lastTimestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                currentTimestamp = waitNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }
        
        lastTimestamp = currentTimestamp;
        
        return ((currentTimestamp - epoch) << timestampShift) |
               (datacenterId << datacenterIdShift) |
               (machineId << machineIdShift) |
               sequence;
    }
    
    private long waitNextMillis(long lastTimestamp) {
        long timestamp = getCurrentTimestamp();
        while (timestamp <= lastTimestamp) {
            timestamp = getCurrentTimestamp();
        }
        return timestamp;
    }
    
    private long getCurrentTimestamp() {
        return CachedTimer.currentTime();
        // return System.currentTimeMillis();
    }
}

テスト環境:

  • Windows: i5-4590 16GB RAM 4コア 512GB SSD
  • Mac: Mac Pro 2020 512GB SSD 16GB RAM
  • Linux: Deepin OS、仮想マシン、160GBディスク、8GB RAM

単一スレッド環境でのSystem.currentTimeMillis()パフォーマンス:

プラットフォーム/件数 10,000 1,000,000 10,000,000 100,000,000
Mac 5 247 2444 24416
Windows 3 249 2448 24426
Linux 135 598 4076 26388

単一スレッド環境でのCachedTimer.currentTime()パフォーマンス:

プラットフォーム/件数 10,000 1,000,000 10,000,000 100,000,000
Mac 52 299 2501 24674
Windows 56 3942 38934 389983
Linux 336 1226 4454 27639

単一スレッドテストでは、バックグラウンド時計の利点が現れず、Windows環境では大量データ処理時に著しく遅くなりました。Linux環境でも改善は見られませんでした。

マルチスレッドテストコード:

public class PerformanceTest {
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 16;
        CountDownLatch latch = new CountDownLatch(threadCount);
        int idsPerThread = 100000000 / threadCount;
        long startTime = System.currentTimeMillis();
        
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1, 1);
                for (int j = 0; j < idsPerThread; j++) {
                    generator.generateId();
                }
                latch.countDown();
            }).start();
        }
        
        latch.await();
        System.out.println("Total time: " + (System.currentTimeMillis() - startTime));
    }
}

マルチスレッド環境でのSystem.currentTimeMillis()パフォーマンス(100,000,000件):

プラットフォーム/スレッド数 2 4 8 16
Mac 14373 6132 3410 3247
Windows 12408 6862 6791 7114
Linux 20753 19055 18919 19602

マルチスレッド環境でのCachedTimer.currentTime()パフォーマンス(100,000,000件):

プラットフォーム/スレッド数 2 4 8 16
Mac 12319 6275 3691 3746
Windows 194763 110442 153960 174974
Linux 26516 25313 25497 25544

マルチスレッド環境では、Macでは大きな変化はありませんでしたが、Windowsでは顕著な性能低下が見られました。LinuxでもSystem.currentTimeMillis()に比べて遅くなっています。

時刻衝突の頻度も重要な指標です:

static AtomicLong collisionCounter = new AtomicLong(0);
private long getCurrentTimestamp() {
    collisionCounter.incrementAndGet();
    return CachedTimer.currentTime();
    // return System.currentTimeMillis();
}

10,000,000件のID生成を8スレッドで実行した際の時刻取得回数:

プラットフォーム/メソッド CachedTimer System.currentTimeMillis
Mac 23067209 12896314
Windows 705460039 35164476
Linux 1165552352 81422626

キャッシュされた時刻を使用する場合、同じ時刻が取得される可能性が高く、時刻衝突が頻繁に発生します。特にLinux環境では衝突回数が著しく増加しています。

結論として、実際のテストではCachedTimer.currentTime()による最適化効果は確認できず、競合により時刻衝突の可能性が増加するだけでした。JDK開発者は長期間にわたるテストと最適化を行っており、彼らの実装は信頼性が高いと言えます。結論として、この最適化手法は有効とは言えません。

技術的な判断を行う際は、主観的な意見に頼らず、実験的な検証や信頼できる情報源を参考にすることが重要です。

タグ: スノーフレーク Java パフォーマンス最適化 System.currentTimeMillis 並行処理

7月22日 21:49 投稿