Open8

楽観的手法を試す

hmarui66hmarui66

比較対象

  • nosync
    • 整合性は気にせず同期をしない
  • pessimistic
    • RWLock で保護
  • optimistic
    • read はバージョン検証、write は lock で競合させる
      • Optimistic Lock Coupling 論文の実装を参考にした
      • int64 の 1bit目を lock フラグ、2bit目以降をバージョン番号として利用
hmarui66hmarui66

nosync

package main

// NoSyncTuple は同期なしでtupleを操作する実装
type NoSyncTuple struct {
	tuple *Tuple
}

func NewNoSyncTuple(data string) *NoSyncTuple {
	return &NoSyncTuple{
		tuple: NewTuple(data),
	}
}

func (nt *NoSyncTuple) Read() (string, error) {
	// 同期なしで直接データを読み取り
	return nt.tuple.data, nil
}

func (nt *NoSyncTuple) Write(newData string) error {
	// 同期なしで直接データを書き換え
	nt.tuple.data = newData
	return nil
}
hmarui66hmarui66

pessimistic

package main

import (
	"sync"
)

// PessimisticTuple は悲観的手法(RWLock)で保護されたtuple
type PessimisticTuple struct {
	tuple *Tuple
	rwMu  sync.RWMutex
}

func NewPessimisticTuple(data string) *PessimisticTuple {
	return &PessimisticTuple{
		tuple: NewTuple(data),
	}
}

func (pt *PessimisticTuple) Read() (string, error) {
	pt.rwMu.RLock()
	defer pt.rwMu.RUnlock()

	// データを読み取り
	return pt.tuple.data, nil
}

func (pt *PessimisticTuple) Write(newData string) error {
	pt.rwMu.Lock()
	defer pt.rwMu.Unlock()

	// データを更新
	pt.tuple.data = newData

	return nil
}
hmarui66hmarui66

optimistic

package main

import (
	"sync/atomic"
)

const (
	LockBit = int64(1) << 0  // 1bit目 (最下位ビット)
)

// OptimisticTuple は楽観的手法で保護されたtuple
type OptimisticTuple struct {
	tuple   *Tuple
	version int64
}

func NewOptimisticTuple(data string) *OptimisticTuple {
	return &OptimisticTuple{
		tuple:   NewTuple(data),
		version: 0,
	}
}

func (ot *OptimisticTuple) Read() (string, error) {
	for {
		// バージョンを読み取り
		ver := atomic.LoadInt64(&ot.version)

		// lockされている場合は再試行
		if ver&LockBit != 0 {
			continue
		}

		// データをコピー
		data := ot.tuple.data

		// バージョンが変更されていないことを確認
		newVer := atomic.LoadInt64(&ot.version)
		if newVer == ver {
			return data, nil
		}
	}
}

func (ot *OptimisticTuple) Write(newData string) error {
	for {
		// 現在のバージョンを読み取り
		oldVer := atomic.LoadInt64(&ot.version)

		// lockされている場合は再試行
		if oldVer&LockBit != 0 {
			continue
		}

		// ロックを取得する
		if !atomic.CompareAndSwapInt64(&ot.version, oldVer, oldVer|LockBit) {
			continue
		}

		// ロックを取得できたのでデータを更新
		ot.tuple.data = newData

		// バージョン番号を増加させてロックを解除
		// LockBitをアトミックに加算することでバージョンを+1しつつロックフラグを落とす
		atomic.AddInt64(&ot.version, LockBit)

		return nil
	}
}
hmarui66hmarui66

評価

  • Apple M1 Max(高性能8コア+ 高効率2コア合計10コア)で検証
  • できるだけアプリは落とした状態
  • ワークロードは読み取りのみ
  • goroutine を 1 〜 10 と変化させて計測

結果

  • nosync と optimistic
    • 4 まではきれいにスケールする
    • 6 でサチる
    • 9 で急激に劣化
  • pessimistic
    • 1 が最高でそこから徐々に落ちていく

考察

  • nosync と optimistic がほぼ同等
    • 怪しさも感じつつ、Optimistic Lock Coupling の論文の Fig 3. もそんな感じの結果が出ているのでそんなもの?
    • 6 でサチるのは?
      • なんやかんやアプリは動いているので仕事に専念できてない可能性
      • そもそも main の goroutine も計測結果の取りまとめなどをしている
    • 9 での劣化は高効率コアの利用に起因しているかも
  • pessimistic
    • キャッシュラインを汚してしまっているためスケールしないと思われる

      • 予想通りではあるが、goroutine 1 が最大スループットなのは意外
    • キャッシュミスなどを見た方が確実だが、mac であるためちょっと見るのが面倒で割愛