github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/bench/microbenchmarks/map/main.go (about)

     1  // Package main
     2  /*
     3   * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved.
     4   */
     5  package main
     6  
     7  import (
     8  	"flag"
     9  	"fmt"
    10  	"os"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/NVIDIA/aistore/cmn/cos"
    15  )
    16  
    17  // =============Preamble================
    18  // This benchmark tests the performance of regular maps versus sync.Maps focusing on the
    19  // primitive operations of get, put, and delete while under contention. For the use case of
    20  // AIStore, we want the read times to be as fast as possible.
    21  //
    22  // The design of the benchmark is as follows:
    23  // 1. Fill the map to a max size (default 1000000)
    24  // 2. Randomly get X (default 200000) elements from the map where p% (default 10%) of them will be a random store
    25  // 3. Randomly delete (max size - min size) elements from the map
    26  // All of these operations will happen concurrently with step 2 having the option to spawn more
    27  // go routines (default 8). Additionally, in all cases, the elements being read from the maps,
    28  // put into the maps, and deleted from the maps will be the same between the two maps.
    29  //
    30  // Example of invocation:
    31  // go run main.go -numread 300000 -maxsize 1000000 -minsize 50000 -putpct 10 -workers 8
    32  // go run main.go -numread 100000 -maxsize 1000000 -minsize 50000 -putpct 20 -workers 20 -shrink=false (no deletion)
    33  
    34  // ===========Results===================
    35  // From the results of the benchmark, sync.Map does appear to out perform regular maps for read operations
    36  // when there is contention from multiple go routines. The performance of sync.Map for put and delete operations are usually slower than
    37  // regular maps, but if we expect > 80% of the requests to be reads, sync.Map will be faster than regular maps.
    38  // Overall, if we expect that the maps will have much higher read requests than put/delete requests, sync.Maps are
    39  // the better option.
    40  
    41  type (
    42  	benchMap interface {
    43  		get(int) int
    44  		put(node)
    45  		delete(int)
    46  		mapType() string
    47  	}
    48  	node struct {
    49  		key, val int
    50  	}
    51  
    52  	// Maps
    53  	regMap struct {
    54  		m map[int]int
    55  		// Regular map RWMutex
    56  		mu sync.RWMutex
    57  	}
    58  
    59  	syncMap struct {
    60  		m sync.Map
    61  	}
    62  	params struct {
    63  		maxSize    int  // Max size of map
    64  		minSize    int  // Shrink size of map
    65  		numread    int  // Number of random reads
    66  		iterations int  // Number of iterations to run
    67  		putpct     int  // Percentage of puts
    68  		workers    int  // Number of Read/Write Goroutines
    69  		doShrink   bool // Enable shrinking (Deleting) of keys in map
    70  	}
    71  )
    72  
    73  var (
    74  	// Params
    75  	runParams params
    76  	flagUsage bool
    77  
    78  	nodeList []node
    79  	readList [][]int
    80  	delList  [][]int
    81  )
    82  
    83  // =================sync.Map===================
    84  func newSyncMap() *syncMap {
    85  	return &syncMap{}
    86  }
    87  
    88  func (m *syncMap) get(key int) int {
    89  	val, _ := m.m.Load(key)
    90  	if val == nil {
    91  		return 0
    92  	}
    93  	return val.(int)
    94  }
    95  
    96  func (m *syncMap) put(n node) {
    97  	m.m.Store(n.key, n.val)
    98  }
    99  
   100  func (m *syncMap) delete(key int) {
   101  	m.m.Delete(key)
   102  }
   103  
   104  func (*syncMap) mapType() string {
   105  	return "Sync.Map"
   106  }
   107  
   108  // =================Regular Map===================
   109  func newRegMap() *regMap {
   110  	return &regMap{make(map[int]int), sync.RWMutex{}}
   111  }
   112  
   113  func (m *regMap) get(key int) int {
   114  	m.mu.RLock()
   115  	val := m.m[key]
   116  	m.mu.RUnlock()
   117  	return val
   118  }
   119  
   120  func (m *regMap) put(n node) {
   121  	m.mu.Lock()
   122  	m.m[n.key] = n.val
   123  	m.mu.Unlock()
   124  }
   125  
   126  func (m *regMap) delete(key int) {
   127  	m.mu.Lock()
   128  	delete(m.m, key)
   129  	m.mu.Unlock()
   130  }
   131  
   132  func (*regMap) mapType() string {
   133  	return "Reg.Map"
   134  }
   135  
   136  func parseCmdLine() (p params, err error) {
   137  	f := flag.NewFlagSet(os.Args[0], flag.ExitOnError) // Discard flags of imported packages
   138  
   139  	// Command line options
   140  	f.BoolVar(&flagUsage, "usage", false, "Show extensive help menu with examples and exit")
   141  	f.BoolVar(&p.doShrink, "shrink", true, "Enable shrink (deletion) of map keys")
   142  	f.IntVar(&p.maxSize, "maxsize", 1000000, "Maximum size of map")
   143  	f.IntVar(&p.minSize, "minsize", 500000, "Shrink size of map")
   144  	f.IntVar(&p.numread, "numread", 300000, "Number of random reads")
   145  	f.IntVar(&p.iterations, "iter", 10, "Number of iterations to run")
   146  	f.IntVar(&p.putpct, "putpct", 10, "Percentage of puts")
   147  	f.IntVar(&p.workers, "workers", 8, "Number of Read/Write go-routines")
   148  
   149  	f.Parse(os.Args[1:])
   150  
   151  	if len(os.Args[1:]) == 0 {
   152  		fmt.Println("Flags: ")
   153  		f.PrintDefaults()
   154  		fmt.Println()
   155  		os.Exit(0)
   156  	}
   157  
   158  	os.Args = []string{os.Args[0]}
   159  	flag.Parse() // Called so that imported packages don't compain
   160  
   161  	if p.minSize > p.maxSize {
   162  		return params{}, fmt.Errorf("minsize %d greater than maxsize %d", p.minSize, p.maxSize)
   163  	}
   164  
   165  	if p.putpct < 0 || p.putpct > 100 {
   166  		return params{}, fmt.Errorf("invalid putpct: %d", p.putpct)
   167  	}
   168  
   169  	return
   170  }
   171  
   172  func init() {
   173  	var err error
   174  	runParams, err = parseCmdLine()
   175  	if err != nil {
   176  		fmt.Fprintln(os.Stdout, "err: "+err.Error())
   177  		os.Exit(2)
   178  	}
   179  
   180  	fmt.Printf("Map size: %d\t Min size: %d\t Read count: %d\t Iterations: %d\t Put Pct: %d\t Workers: %d\n\n",
   181  		runParams.maxSize, runParams.minSize, runParams.numread, runParams.iterations, runParams.putpct, runParams.workers)
   182  	nodeList = createNodeList(runParams.maxSize)
   183  	readList = createReadDelList(nodeList, runParams.numread, runParams.iterations)
   184  	delList = createReadDelList(nodeList, runParams.maxSize-runParams.minSize, runParams.iterations)
   185  }
   186  
   187  /* Algorithm "xor" from p. 4 of Marsaglia, "Xorshift RNGs" */
   188  func randInt(r int) int {
   189  	r ^= r << 13
   190  	r ^= r >> 17
   191  	r ^= r << 5
   192  	return r & 0x7fffffff
   193  }
   194  
   195  // Create a node struct where key and value are the same (it should not matter)
   196  func createNodeList(size int) (nodeList []node) {
   197  	nodeList = make([]node, 0, size)
   198  	for i := range size {
   199  		nodeList = append(nodeList, node{key: i, val: randInt(i)})
   200  	}
   201  	return
   202  }
   203  
   204  // Creates a list of keys (int)
   205  func createList(nodeList []node, size int) (keys []int) {
   206  	nodeLen := len(nodeList)
   207  	keys = make([]int, 0, size)
   208  
   209  	for range size {
   210  		keys = append(keys, cos.NowRand().Intn(nodeLen))
   211  	}
   212  	return
   213  }
   214  
   215  // Creates a list of list of keys
   216  func createReadDelList(nodeList []node, size, iterations int) (keysList [][]int) {
   217  	wg := &sync.WaitGroup{}
   218  
   219  	keysList = make([][]int, iterations)
   220  	wg.Add(iterations)
   221  	for i := range iterations {
   222  		go func(index int) {
   223  			keysList[index] = createList(nodeList, size)
   224  			wg.Done()
   225  		}(i)
   226  	}
   227  	wg.Wait()
   228  	return
   229  }
   230  
   231  func fill(m benchMap, nodeList []node) time.Duration {
   232  	start := time.Now()
   233  	for _, ele := range nodeList {
   234  		m.put(ele)
   235  	}
   236  	return time.Since(start)
   237  }
   238  
   239  func shrink(m benchMap, keys []int) time.Duration {
   240  	start := time.Now()
   241  	for _, ele := range keys {
   242  		m.delete(ele)
   243  	}
   244  	return time.Since(start)
   245  }
   246  
   247  func readNWrite(m benchMap, threshold int, keys []int, dur chan time.Duration) {
   248  	start := time.Now()
   249  	for _, key := range keys {
   250  		// Randomly have some puts
   251  		if key%100 >= (100-threshold) || key%100 <= (-100+threshold) {
   252  			m.put(node{key: key, val: key})
   253  		} else {
   254  			m.get(key)
   255  		}
   256  	}
   257  	dur <- time.Since(start)
   258  }
   259  
   260  // Benchmark
   261  // 1. Fill map
   262  // 2. Concurrently run X go routines that reads and occasionally writes
   263  // 3. Shrink
   264  func bench(m benchMap, nodeList []node, readList, delList [][]int, iterations, threshold, workers int, doShrink bool) {
   265  	var (
   266  		wg           = &sync.WaitGroup{}
   267  		fillDur      time.Duration
   268  		shrinkDur    time.Duration
   269  		readWriteDur time.Duration
   270  		totDur       time.Duration
   271  
   272  		totFil    time.Duration
   273  		totRW     time.Duration
   274  		totShrink time.Duration
   275  		totTot    time.Duration
   276  	)
   277  	fmt.Printf("Fill, Read + Random Write and Shrink with %s\n", m.mapType())
   278  
   279  	for i := range iterations {
   280  		start := time.Now()
   281  		readDurCh := make(chan time.Duration, workers)
   282  		wg.Add(1)
   283  		go func() {
   284  			fillDur = fill(m, nodeList)
   285  			wg.Done()
   286  		}()
   287  
   288  		for range workers {
   289  			wg.Add(1)
   290  			go func(idx int) {
   291  				readNWrite(m, threshold, readList[idx], readDurCh)
   292  				wg.Done()
   293  			}(i)
   294  		}
   295  
   296  		if doShrink {
   297  			wg.Add(1)
   298  			go func(idx int) {
   299  				shrinkDur = shrink(m, delList[idx])
   300  				wg.Done()
   301  			}(i)
   302  		}
   303  		wg.Wait()
   304  		close(readDurCh)
   305  		for dur := range readDurCh {
   306  			readWriteDur += dur
   307  		}
   308  		totDur = time.Since(start)
   309  
   310  		averageRW := time.Duration(int(readWriteDur) / workers)
   311  		fmt.Printf("%s | Fill: %s\t Read+Write: %s\t Shrink: %s\t Total: %s\n",
   312  			m.mapType(), fillDur, averageRW, shrinkDur, totDur)
   313  		readWriteDur = 0
   314  		totFil += fillDur
   315  		totRW += averageRW
   316  		totShrink += shrinkDur
   317  		totTot += totDur
   318  	}
   319  	fmt.Println()
   320  	fmt.Printf("%s | Avg. Fill: %s\t Avg. Read+Write: %s\t Avg. Shrink: %s\t Avg. Total: %s\n",
   321  		m.mapType(),
   322  		time.Duration(int(totFil)/iterations),
   323  		time.Duration(int(totRW)/iterations),
   324  		time.Duration(int(totShrink)/iterations),
   325  		time.Duration(int(totTot)/iterations))
   326  	fmt.Println()
   327  }
   328  
   329  func main() {
   330  	sMap := newSyncMap()
   331  	rMap := newRegMap()
   332  
   333  	bench(sMap, nodeList, readList, delList, runParams.iterations, runParams.putpct, runParams.workers, runParams.doShrink)
   334  	bench(rMap, nodeList, readList, delList, runParams.iterations, runParams.putpct, runParams.workers, runParams.doShrink)
   335  }