diff --git a/README.md b/README.md index ebb1d63..9f5c3e5 100644 --- a/README.md +++ b/README.md @@ -28,119 +28,74 @@ go get github.com/marpit19/goquickmap ## Usage -### QuickMap +Note: All keys in the current implementation must be strings. +### QuickMap ```go -import "github.com/marpit19/goquickmap/pkg/quickmap" - -// Create a new QuickMap with default capacity m := quickmap.New() - -// Create a QuickMap with a specific initial capacity -m := quickmap.NewWithCapacity(1000) - -// Insert a key-value pair -m.Insert("key", "value") - -// Get a value +m.Insert("key", "value") // key must be a string value, exists := m.Get("key") -// Delete a key-value pair -m.Delete("key") - -// Batch insert -pairs := map[string]interface{}{ - "key1": "value1", - "key2": "value2", -} -m.InsertMany(pairs) - -// Batch delete -keys := []string{"key1", "key2"} -m.DeleteMany(keys) -``` +## Performance -### QuickSet +GoQuickMap offers significant performance improvements over built-in Go maps and popular third-party set implementations. Here's a comparison based on 1,000,000 operations: -```go -import "github.com/marpit19/goquickmap/pkg/quickset" +### Map Operations -// Create a new QuickSet with default capacity -s := quickset.New() +| Operation | Built-in Map | QuickMap | Improvement | +|--------------|--------------|-------------|-------------| +| Insert | 271.17ms | 236.95ms | 12.6% faster | +| Get | 153.89ms | 83.68ms | 45.6% faster | +| Delete | 188.82ms | 70.47ms | 62.7% faster | -// Create a QuickSet with a specific initial capacity -s := quickset.NewWithCapacity(1000) +QuickMap also supports efficient batch operations: +- Batch Insert (10,000 items): 638.54µs +- Batch Delete (10,000 items): 106.5µs -// Add an element -s.Add("element") +### Set Operations -// Check if an element exists -exists := s.Contains("element") +| Operation | golang-set | QuickSet | Improvement | +|--------------|--------------|-------------|-------------| +| Add | 231.33ms | 211.49ms | 8.6% faster | +| Contains | 258.16ms | 88.20ms | 65.8% faster | +| Remove | 232.36ms | 78.90ms | 66.0% faster | -// Remove an element -s.Remove("element") +QuickSet also supports efficient batch operations: +- Batch Add (10,000 items): 1.26ms +- Batch Remove (10,000 items): 111.67µs -// Batch add -elements := []string{"elem1", "elem2"} -s.AddMany(elements) +### Analysis -// Batch remove -s.RemoveMany(elements) -``` +1. **Superior Performance**: GoQuickMap consistently outperforms built-in maps and popular set implementations across all operations. -### QuickDict +2. **Significant Speedup for Lookups and Deletions**: QuickMap and QuickSet show dramatic improvements in Get/Contains (45-65% faster) and Delete/Remove operations (62-66% faster). -```go -import "github.com/marpit19/goquickmap/pkg/quickdict" +3. **Efficient Insertions**: Both QuickMap and QuickSet demonstrate faster insertion times compared to their counterparts. -// Create a new QuickDict with default capacity -d := quickdict.New() +4. **Batch Operations**: The library offers highly efficient batch operations, allowing for rapid insertion and deletion of multiple items simultaneously. -// Create a QuickDict with a specific initial capacity -d := quickdict.NewWithCapacity(1000) +5. **Consistent Advantage**: The performance advantage is maintained across different types of operations, indicating a well-optimized underlying structure. -// Set a key-value pair -d.Set("key", "value") +These results demonstrate that GoQuickMap is an excellent choice for applications requiring high-performance hash tables, maps, or sets, especially those dealing with large datasets or frequent lookup and deletion operations. -// Get a value -value, exists := d.Get("key") +## Current Limitations and Future Plans -// Delete a key-value pair -d.Delete("key") +### String Keys Only +The current implementation of GoQuickMap, including QuickMap, QuickSet, and QuickDict, only supports string keys. This design choice was made to optimize performance for string-based keys, which are common in many applications. -// Batch set -pairs := map[string]interface{}{ - "key1": "value1", - "key2": "value2", -} -d.SetMany(pairs) +#### Implications: +1. **Use Case Focus**: The library is currently best suited for applications that primarily use string identifiers or textual data as keys. +2. **Performance Optimization**: The string-specific implementation allows for optimizations that may not be possible with a more generic approach. +3. **Benchmark Context**: The performance comparisons provided are specifically for string keys and may vary for other types of keys. -// Batch delete -keys := []string{"key1", "key2"} -d.DeleteMany(keys) -``` +### Future Considerations +We acknowledge that supporting only string keys is a limitation. Potential future enhancements may include: -## Performance +1. **Generic Key Support**: Implementing support for generic types as keys, allowing for greater flexibility. +2. **Numeric Key Optimization**: Adding specialized implementations for common numeric types (int, int64, etc.) that could potentially offer even better performance for these types. +3. **Custom Hash Functions**: Allowing users to provide custom hash functions for their specific key types. -GoQuickMap is designed for high performance. Here are some benchmark results on an Apple M3 Pro: - -- QuickMap Insert: ~383 ns/op -- QuickMap Get: ~26 ns/op -- QuickMap Delete: ~26 ns/op -- QuickMap InsertMany (1000 items): ~172 µs/op -- QuickMap DeleteMany (1000 items): ~4.2 µs/op - -- QuickSet Add: ~316 ns/op -- QuickSet Contains: ~25 ns/op -- QuickSet Remove: ~26 ns/op -- QuickSet AddMany (1000 items): ~115 µs/op -- QuickSet RemoveMany (1000 items): ~4.2 µs/op - -- QuickDict Set: ~360 ns/op -- QuickDict Get: ~26 ns/op -- QuickDict Delete: ~25 ns/op -- QuickDict SetMany (1000 items): ~171 µs/op -- QuickDict DeleteMany (1000 items): ~4.2 µs/op +We welcome feedback and contributions from the community regarding these potential improvements. If you have specific use cases that require non-string keys, please open an issue to discuss your needs. ## Contributing diff --git a/cmd/performance/main.go b/cmd/performance/main.go new file mode 100644 index 0000000..724f987 --- /dev/null +++ b/cmd/performance/main.go @@ -0,0 +1,193 @@ +/* +package main + +import ( + "fmt" + "math/rand" + "runtime" + "strconv" + "time" + + mapset "github.com/deckarep/golang-set/v2" + "github.com/marpit19/goquickmap/pkg/quickmap" + "github.com/marpit19/goquickmap/pkg/quickset" +) + +const ( + numOperations = 1000000 + numBatchOperations = 10000 +) + +func main() { + fmt.Println("Performance Comparison") + fmt.Printf("Number of operations: %d\n", numOperations) + fmt.Printf("Number of batch operations: %d\n", numBatchOperations) + + compareMap() + compareSet() +} + +func compareMap() { + fmt.Println("\n--- Map Comparison ---") + + // Built-in map + start := time.Now() + m := make(map[string]int) + for i := 0; i < numOperations; i++ { + key := strconv.Itoa(i) + m[key] = i + } + builtinInsertTime := time.Since(start) + + start = time.Now() + for i := 0; i < numOperations; i++ { + key := strconv.Itoa(i) + _ = m[key] + } + builtinGetTime := time.Since(start) + + start = time.Now() + for i := 0; i < numOperations; i++ { + key := strconv.Itoa(i) + delete(m, key) + } + builtinDeleteTime := time.Since(start) + + // QuickMap + start = time.Now() + qm := quickmap.New() + for i := 0; i < numOperations; i++ { + key := strconv.Itoa(i) + qm.Insert(key, i) + } + quickmapInsertTime := time.Since(start) + + start = time.Now() + for i := 0; i < numOperations; i++ { + key := strconv.Itoa(i) + _, _ = qm.Get(key) + } + quickmapGetTime := time.Since(start) + + start = time.Now() + for i := 0; i < numOperations; i++ { + key := strconv.Itoa(i) + qm.Delete(key) + } + quickmapDeleteTime := time.Since(start) + + // Batch operations + batchKeys := make([]string, numBatchOperations) + batchMap := make(map[string]interface{}, numBatchOperations) + for i := 0; i < numBatchOperations; i++ { + key := strconv.Itoa(rand.Intn(numOperations)) + batchKeys[i] = key + batchMap[key] = i + } + + start = time.Now() + qm.InsertMany(batchMap) + quickmapBatchInsertTime := time.Since(start) + + start = time.Now() + qm.DeleteMany(batchKeys) + quickmapBatchDeleteTime := time.Since(start) + + // Print results + fmt.Println("Built-in map:") + fmt.Printf(" Insert: %v\n", builtinInsertTime) + fmt.Printf(" Get: %v\n", builtinGetTime) + fmt.Printf(" Delete: %v\n", builtinDeleteTime) + + fmt.Println("QuickMap:") + fmt.Printf(" Insert: %v\n", quickmapInsertTime) + fmt.Printf(" Get: %v\n", quickmapGetTime) + fmt.Printf(" Delete: %v\n", quickmapDeleteTime) + fmt.Printf(" Batch Insert (%d items): %v\n", numBatchOperations, quickmapBatchInsertTime) + fmt.Printf(" Batch Delete (%d items): %v\n", numBatchOperations, quickmapBatchDeleteTime) +} + +func compareSet() { + fmt.Println("\n--- Set Comparison ---") + + // golang-set + start := time.Now() + s := mapset.NewSet[string]() + for i := 0; i < numOperations; i++ { + s.Add(strconv.Itoa(i)) + } + mapsetAddTime := time.Since(start) + + start = time.Now() + for i := 0; i < numOperations; i++ { + s.Contains(strconv.Itoa(i)) + } + mapsetContainsTime := time.Since(start) + + start = time.Now() + for i := 0; i < numOperations; i++ { + s.Remove(strconv.Itoa(i)) + } + mapsetRemoveTime := time.Since(start) + + // QuickSet + start = time.Now() + qs := quickset.New() + for i := 0; i < numOperations; i++ { + qs.Add(strconv.Itoa(i)) + } + quicksetAddTime := time.Since(start) + + start = time.Now() + for i := 0; i < numOperations; i++ { + qs.Contains(strconv.Itoa(i)) + } + quicksetContainsTime := time.Since(start) + + start = time.Now() + for i := 0; i < numOperations; i++ { + qs.Remove(strconv.Itoa(i)) + } + quicksetRemoveTime := time.Since(start) + + // Batch operations + batchElements := make([]string, numBatchOperations) + for i := 0; i < numBatchOperations; i++ { + batchElements[i] = strconv.Itoa(rand.Intn(numOperations)) + } + + start = time.Now() + qs.AddMany(batchElements) + quicksetBatchAddTime := time.Since(start) + + start = time.Now() + qs.RemoveMany(batchElements) + quicksetBatchRemoveTime := time.Since(start) + + // Print results + fmt.Println("golang-set:") + fmt.Printf(" Add: %v\n", mapsetAddTime) + fmt.Printf(" Contains: %v\n", mapsetContainsTime) + fmt.Printf(" Remove: %v\n", mapsetRemoveTime) + + fmt.Println("QuickSet:") + fmt.Printf(" Add: %v\n", quicksetAddTime) + fmt.Printf(" Contains: %v\n", quicksetContainsTime) + fmt.Printf(" Remove: %v\n", quicksetRemoveTime) + fmt.Printf(" Batch Add (%d items): %v\n", numBatchOperations, quicksetBatchAddTime) + fmt.Printf(" Batch Remove (%d items): %v\n", numBatchOperations, quicksetBatchRemoveTime) +} + +func printMemUsage() { + var m runtime.MemStats + runtime.ReadMemStats(&m) + fmt.Printf("Alloc = %v MiB", bToMb(m.Alloc)) + fmt.Printf("\tTotalAlloc = %v MiB", bToMb(m.TotalAlloc)) + fmt.Printf("\tSys = %v MiB", bToMb(m.Sys)) + fmt.Printf("\tNumGC = %v\n", m.NumGC) +} + +func bToMb(b uint64) uint64 { + return b / 1024 / 1024 +} + */ \ No newline at end of file diff --git a/images/benchmark3.png b/images/benchmark3.png new file mode 100644 index 0000000..3d70a43 Binary files /dev/null and b/images/benchmark3.png differ