Skip to content

Commit

Permalink
DEBT: Performance and Memory Allocation Optimization (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
mprimeaux authored Oct 26, 2024
1 parent 3428dd3 commit 8b8b4b6
Show file tree
Hide file tree
Showing 6 changed files with 355 additions and 212 deletions.
21 changes: 19 additions & 2 deletions CHANGELOG/CHANGELOG-1.x.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
### Security

---
## [1.4.0] - 2024-OCT-26

### Added
- **FEATURE:**: Added concurrent benchmark tests.
### Changed
- **DEBT:** Maintained Safety with Linter Suppression: Added `// nolint:gosec` with justification for safe conversions.
- **DEBT:** Refactored Slice Initialization: Initialized `idRunes` with zero length and pre-allocated capacity, using append to build the slice.
- **DEBT:** Ensured Comprehensive Testing: Reviewed and updated tests to handle all edge cases and ensure no runtime errors.
### Deprecated
### Removed
- **FEATURE:** Removed Unicode support for custom dictionaries.
### Fixed
- **DEFECT:** Fixed Operator Precedence: Changed `bits.Len(uint(alphabetLen - 1))` to `bits.Len(uint(alphabetLen) - 1)` to ensure safe conversion.
### Security

---
## [1.3.0] - 2024-OCT-26

Expand All @@ -35,7 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Deprecated
### Removed
### Fixed
- **DFECT:** Fixed version compare links in CHANGELOG.
- **DEFECT:** Fixed version compare links in CHANGELOG.
### Security

---
Expand All @@ -49,7 +65,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
### Security

[Unreleased]: https://github.com/scriptures-social/platform/compare/v1.3.0...HEAD
[Unreleased]: https://github.com/scriptures-social/platform/compare/v1.4.0...HEAD
[1.4.0]: https://github.com/sixafter/nanoid/compare/v1.3.0...v1.4.0
[1.3.0]: https://github.com/sixafter/nanoid/compare/v1.2.0...v1.3.0
[1.2.0]: https://github.com/sixafter/nanoid/compare/v1.0.0...v1.2.0
[1.0.0]: https://github.com/sixafter/nanoid/compare/a6a1eb74b61e518fd0216a17dfe5c9b4c432e6e8...v1.0.0
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ test: ## Execute unit tests

.PHONY: bench
bench: ## Execute benchmark tests
$(GO_TEST) -bench=.
$(GO_TEST) -bench=. -benchmem ./...

.PHONY: clean
clean: ## Remove previous build
Expand Down
89 changes: 65 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@ A simple, fast, and efficient Go implementation of [NanoID](https://github.com/a

## Features

- **Secure**: Uses `crypto/rand` for cryptographically secure random number generation.
- **Fast**: Optimized for performance with efficient algorithms.
- **Thread-Safe**: Safe for concurrent use in multi-threaded applications.
- **Customizable**: Specify custom ID lengths and alphabets.
- **Easy to Use**: Simple API with sensible defaults.
* **Stateless Design**: Each function operates independently without relying on global state or caches, eliminating the need for synchronization primitives like mutexes. This design ensures predictable behavior and simplifies usage in various contexts.
* **Cryptographically Secure**: Utilizes Go's crypto/rand package for generating cryptographically secure random numbers. This guarantees that the generated IDs are both unpredictable and suitable for security-sensitive applications.
* **High Performance**: Optimized algorithms and efficient memory management techniques ensure rapid ID generation. Whether you're generating a few IDs or millions, the library maintains consistent speed and responsiveness.
* **Memory Efficient**: Implements sync.Pool to reuse byte slices, minimizing memory allocations and reducing garbage collection overhead. This approach significantly enhances performance, especially in high-throughput scenarios.
* **Thread-Safe**: Designed for safe concurrent use in multi-threaded applications. Multiple goroutines can generate IDs simultaneously without causing race conditions or requiring additional synchronization.
* **Customizable**: Offers flexibility to specify custom ID lengths and alphabets. Whether you need short, compact IDs or longer, more complex ones, the library can accommodate your specific requirements.
* **User-Friendly API**: Provides a simple and intuitive API with sensible defaults, making integration straightforward. Developers can start generating IDs with minimal configuration and customize as needed.
* **Zero External Dependencies**: Relies solely on Go's standard library, ensuring ease of use, compatibility, and minimal footprint within your projects.
* **Comprehensive Testing**: Includes a robust suite of unit tests and concurrency tests to ensure reliability, correctness, and thread safety. This commitment to quality guarantees consistent performance across different use cases.
* **Detailed Documentation**: Accompanied by clear and thorough documentation, including examples and usage guidelines. New users can quickly understand how to implement and customize the library to fit their needs.
* **Efficient Error Handling**: Employs predefined errors to avoid unnecessary allocations, enhancing both performance and clarity in error management.
* **Optimized for Low Allocations**: Carefully structured to minimize heap allocations, reducing memory overhead and improving cache locality. This optimization is crucial for applications where performance and resource usage are critical.

## Installation

Expand Down Expand Up @@ -61,27 +68,14 @@ fmt.Println("Generated Nano ID of size 32:", id)
Generate a Nano ID using a custom alphabet:

```go
customAlphabet := "abcdef123456"
id, err := nanoid.NewCustom(16, customAlphabet)
alphabet := "abcdef123456"
id, err := nanoid.NewCustom(16, alphabet)
if err != nil {
log.Fatal(err)
}
fmt.Println("Generated Nano ID with custom alphabet:", id)
```

### Generate a Nano ID with Unicode Alphabet

Generate a Nano ID using a Unicode alphabet:

```go
unicodeAlphabet := "あいうえお漢字🙂🚀"
id, err := nanoid.NewCustom(10, unicodeAlphabet)
if err != nil {
log.Fatal(err)
}
fmt.Println("Generated Nano ID with Unicode alphabet:", id)
```

### Generate a Nano ID with Custom Random Source

Generate a Nano ID using a custom random source that implements io.Reader:
Expand Down Expand Up @@ -148,17 +142,64 @@ func main() {
* `DefaultAlphabet`: The default alphabet used for ID generation: `-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz`
* `DefaultSize`: The default size of the generated ID: `21`

## Unicode Support

This implementation fully supports custom alphabets containing Unicode characters, including emojis and characters from various languages. By using []rune internally, it correctly handles multi-byte Unicode characters.

## Performance

The package is optimized for performance and low memory consumption:
* **Efficient Random Byte Consumption**: Uses bitwise operations to extract random bits efficiently.
* **Avoids `math/big`**: Does not use `math/big`, relying on built-in integer types for calculations.
* **Minimized System Calls**: Reads random bytes in batches to reduce the number of system calls.

## Execute Benchmarks:

Run the benchmarks using the go test command with the `bench` make target:

```shell
make bench
```

### Interpreting Results:

Sample output might look like this:

```shell
go test -bench=. -benchmem ./...
goos: darwin
goarch: arm64
pkg: github.com/sixafter/nanoid
cpu: Apple M3 Max
BenchmarkNew-16 6329498 189.2 ns/op 40 B/op 3 allocs/op
BenchmarkNewSize/Size10-16 11600679 102.4 ns/op 24 B/op 2 allocs/op
BenchmarkNewSize/Size21-16 6384469 186.7 ns/op 40 B/op 3 allocs/op
BenchmarkNewSize/Size50-16 2680179 448.2 ns/op 104 B/op 6 allocs/op
BenchmarkNewSize/Size100-16 1387914 863.3 ns/op 192 B/op 11 allocs/op
BenchmarkNewCustom/Size10_CustomASCIIAlphabet-16 9306187 128.8 ns/op 24 B/op 2 allocs/op
BenchmarkNewCustom/Size21_CustomASCIIAlphabet-16 5062975 239.4 ns/op 40 B/op 3 allocs/op
BenchmarkNewCustom/Size50_CustomASCIIAlphabet-16 2322037 515.3 ns/op 101 B/op 5 allocs/op
BenchmarkNewCustom/Size100_CustomASCIIAlphabet-16 1235755 972.0 ns/op 182 B/op 9 allocs/op
BenchmarkNew_Concurrent/Concurrency1-16 2368245 513.1 ns/op 40 B/op 3 allocs/op
BenchmarkNew_Concurrent/Concurrency2-16 1940826 609.5 ns/op 40 B/op 3 allocs/op
BenchmarkNew_Concurrent/Concurrency4-16 1986049 585.6 ns/op 40 B/op 3 allocs/op
BenchmarkNew_Concurrent/Concurrency8-16 1999959 602.2 ns/op 40 B/op 3 allocs/op
BenchmarkNew_Concurrent/Concurrency16-16 2018793 595.6 ns/op 40 B/op 3 allocs/op
BenchmarkNewCustom_Concurrent/Concurrency1-16 1960315 611.7 ns/op 40 B/op 3 allocs/op
BenchmarkNewCustom_Concurrent/Concurrency2-16 1790460 673.7 ns/op 40 B/op 3 allocs/op
BenchmarkNewCustom_Concurrent/Concurrency4-16 1766841 670.7 ns/op 40 B/op 3 allocs/op
BenchmarkNewCustom_Concurrent/Concurrency8-16 1768189 677.4 ns/op 40 B/op 3 allocs/op
BenchmarkNewCustom_Concurrent/Concurrency16-16 1765303 689.5 ns/op 40 B/op 3 allocs/op
PASS
ok github.com/sixafter/nanoid 33.279s
```

* `ns/op` (Nanoseconds per Operation):
* Indicates the average time taken per operation.
* Lower values signify better CPU performance.
* `B/op` (Bytes Allocated per Operation):
* Shows the average number of bytes allocated per operation.
* `0 B/op` indicates no heap allocations, which is optimal.
* `allocs/op` (Allocations per Operation):
* Represents the average number of memory allocations per operation.
* `0 allocs/op` is ideal as it indicates no heap allocations.

## Contributing

Contributions are welcome! Please feel free to submit issues or pull requests.
Expand Down
111 changes: 61 additions & 50 deletions nanoid.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,107 +2,118 @@
//
// This source code is licensed under the MIT License found in the
// LICENSE file in the root directory of this source tree.

// nanoid.go
package nanoid

import (
"crypto/rand"
"errors"
"io"
"math/bits"
"strings"
"sync"
)

// Constants for default settings.
const (
DefaultAlphabet = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"
DefaultSize = 21
MaxUintSize = 1024 // Adjust as needed
)

// Predefined errors to avoid allocations on each call.
var (
ErrInvalidSize = errors.New("size must be greater than zero")
ErrSizeExceedsMaxUint = errors.New("size exceeds maximum allowed value")
ErrEmptyAlphabet = errors.New("alphabet must not be empty")
ErrRandomSourceNoData = errors.New("random source returned no data")
)

// New generates a NanoID with the default size and alphabet using crypto/rand as the random source.
// Byte pool to reuse byte slices and minimize allocations.
var bytePool = sync.Pool{
New: func() interface{} {
b := make([]byte, MaxUintSize) // Non-zero length and capacity

return &b
},
}

// New generates a Nano ID with the default size and alphabet using crypto/rand as the random source.
func New() (string, error) {
return NewSize(DefaultSize)
}

// NewSize generates a NanoID with a specified size and the default alphabet using crypto/rand as the random source.
// NewSize generates a Nano ID with a specified size and the default alphabet using crypto/rand as the random source.
func NewSize(size int) (string, error) {
return NewCustom(size, DefaultAlphabet)
}

// NewCustom generates a NanoID with a specified size and custom alphabet using crypto/rand as the random source.
// NewCustom generates a Nano ID with a specified size and custom ASCII alphabet using crypto/rand as the random source.
func NewCustom(size int, alphabet string) (string, error) {
return NewCustomReader(size, alphabet, cryptoRandReader)
}

// NewCustomReader generates a NanoID with a specified size, custom alphabet, and custom random source.
func NewCustomReader(size int, alphabet string, rnd io.Reader) (string, error) {
if rnd == nil {
return "", errors.New("random source cannot be nil")
}
if size <= 0 {
return "", errors.New("size must be greater than zero")
return "", ErrInvalidSize
}

// Convert alphabet to []rune to support Unicode characters
alphabetRunes := []rune(alphabet)
alphabetLen := len(alphabetRunes)
if alphabetLen == 0 {
return "", errors.New("alphabet must not be empty")
if size > MaxUintSize {
return "", ErrSizeExceedsMaxUint
}

// Handle special case when alphabet length is 1
if alphabetLen == 1 {
return strings.Repeat(string(alphabetRunes[0]), size), nil
if len(alphabet) == 0 {
return "", ErrEmptyAlphabet
}

// Calculate the number of bits needed to represent the alphabet indices
bitsPerChar := bits.Len(uint(alphabetLen - 1))
return generateASCIIID(size, alphabet)
}

// generateASCIIID generates an ID using a byte-based (ASCII) alphabet.
func generateASCIIID(size int, alphabet string) (string, error) {
//nolint:gosec // G115: conversion from int to uint is safe due to prior bounds checking
bitsPerChar := bits.Len(uint(len(alphabet) - 1))
if bitsPerChar == 0 {
bitsPerChar = 1
}

idRunes := make([]rune, size)
// Acquire a pointer to a byte slice from the pool
bufPtr, ok := bytePool.Get().(*[]byte)
if !ok {
panic("bytePool.Get() did not return a *[]byte")
}
buf := *bufPtr
buf = buf[:size] // Slice to desired size

defer func() {
// Reset the slice back to MaxUintSize before putting it back
*bufPtr = (*bufPtr)[:MaxUintSize]
bytePool.Put(bufPtr)
}()

var bitBuffer uint64
var bitsInBuffer int
i := 0

for i < size {
// If we don't have enough bits, read more random bytes
for i := 0; i < size; {
if bitsInBuffer < bitsPerChar {
var b [8]byte // Read up to 8 bytes at once for efficiency
n, err := rnd.Read(b[:])
var b [8]byte
n, err := rand.Read(b[:])
if err != nil {
return "", err
}
if n == 0 {
return "", errors.New("random source returned no data")
return "", ErrRandomSourceNoData
}
// Append the new random bytes to the bit buffer
for j := 0; j < n; j++ {
bitBuffer |= uint64(b[j]) << bitsInBuffer
bitsInBuffer += 8
}
}

// Extract bitsPerChar bits to get the index
idx := int(bitBuffer & ((1 << bitsPerChar) - 1))
mask := uint64((1 << bitsPerChar) - 1)
idx := bitBuffer & mask
bitBuffer >>= bitsPerChar
bitsInBuffer -= bitsPerChar

// Use the index if it's within the alphabet range
if idx < alphabetLen {
idRunes[i] = alphabetRunes[idx]
//nolint:gosec // G115: conversion from int to uint is safe due to prior bounds checking
if int(idx) < len(alphabet) {
buf[i] = alphabet[idx]
i++
}
// Else discard and continue
}

return string(idRunes), nil
}

// cryptoRandReader is a wrapper around crypto/rand.Reader to match io.Reader interface.
var cryptoRandReader io.Reader = cryptoRandReaderType{}

type cryptoRandReaderType struct{}

func (cryptoRandReaderType) Read(p []byte) (int, error) {
return rand.Read(p)
return string(buf), nil
}
Loading

0 comments on commit 8b8b4b6

Please sign in to comment.