Benchmarks
Performance Benchmarks: Generic vs Reflection
Last Updated: 2026-01-20
Quick Summary
We added 38 generic assertion functions to testify v2, providing type-safe alternatives to reflection-based assertions. While the primary goal was compile-time type safety, comprehensive benchmarking revealed an unexpected bonus: dramatic performance improvements.
Key Results:
- Type Safety: Catch errors when writing tests, not when running them
- Performance: 1.2x to 81x faster depending on the operation
- Memory: Up to 99% reduction in allocations for collection operations
- Zero Downside: Generic variants are always as fast or faster
The Type Safety Story
The main reason for adding generics wasn’t performanceβit was catching bugs earlier.
Before: Runtime Surprises
After: Compile-Time Safety
Real-world benefit: When refactoring changes a type from []int to []string, generic assertions immediately flag all broken tests. Reflection-based assertions compile but fail during test runsβor worse, pass with wrong comparisons.
Performance Highlights
While type safety was the goal, benchmarking revealed impressive performance gains across all domains.
π Collection Operations: The Big Winner
| Function | Speedup | Memory Savings |
|---|---|---|
| ElementsMatch (10 items) | 21x faster | 568 B β 320 B (44% reduction) |
| ElementsMatch (100 items) | 39x faster | 41 KB β 3.6 KB (91% reduction) |
| ElementsMatch (1000 items) | 81x faster | 4 MB β 33 KB (99% reduction) |
| SliceContains | 16x faster | 4 allocs β 0 |
| SeqContains (iter.Seq) | 25x faster | 55 allocs β 9 |
| SliceSubset | 43x faster | 17 allocs β 0 |
Why it matters: Collection operations are common in tests. ElementsMatchT is up to 81x faster and uses 99% less memory for large slices.
β‘ Comparison Operations
| Function | Speedup | Benefit |
|---|---|---|
| Greater/Less | 10-15x faster | Zero allocations |
| Positive/Negative | 16-22x faster | Zero allocations |
| GreaterOrEqual/LessOrEqual | 10-11x faster | Zero allocations |
Why it matters: Direct operator usage (>, <) eliminates reflection overhead and boxing.
β Equality Checks
| Function | Speedup | Notes |
|---|---|---|
| Equal | 10-13x faster | All numeric types, strings |
| NotEqual | 11x faster | Zero allocations |
| IsOfType | 9-11x faster | Type checks without reflection |
Why it matters: Equality checks are the most common assertion. 10x speedup adds up quickly.
π Ordering Operations
| Function | Speedup | Notes |
|---|---|---|
| IsIncreasing | 7.4x faster | 11 allocs β 0 |
| IsDecreasing | 9.5x faster | 11 allocs β 0 |
| IsNonIncreasing | 6.5x faster | 4 allocs β 0 |
| IsNonDecreasing | 8x faster | 4 allocs β 0 |
Why it matters: Ordering checks iterate over collections. Generics eliminate per-element reflection overhead.
What This Means for You
Always Prefer Generic Variants
When a generic variant is available (functions ending in T), use it:
Type Safety Catches Real Bugs
Example 1: Refactoring Safety
Example 2: IDE Assistance
Generic variants enable IDE autocomplete to suggest only correctly-typed variables, preventing copy-paste errors.
When to Use Reflection Variants
Keep reflection-based assertions for:
- Intentional cross-type comparisons (e.g.,
intvsint64with EqualValues) - Heterogeneous collections (
[]any) - Dynamic type scenarios where compile-time type is unknown
- Backward compatibility with existing tests
Performance Tiers
Tier 1: Dramatic Improvements (10x+)
These operations see the biggest speedups because reflection overhead dominates:
- ElementsMatch: 21-81x (scales with collection size)
- Equal/NotEqual: 10-13x
- Comparison operators: 10-22x
- Type checks: 9-11x
Tier 2: Significant Improvements (3-10x)
Solid gains from eliminating reflection:
- Ordering checks: 6.5-9.5x
- Collection operations: 7.5-43x
Tier 3: Modest Improvements (1.2-3x)
Operations already optimized see smaller gains:
- Same/NotSame: 1.5-2x
- Numeric comparisons: 1.2-1.5x
- Boolean checks: 2x
Tier 4: Comparable Performance
Operations where expensive processing dominates:
- JSONEq: JSON parsing dominates (marginal difference)
- Regexp: Regex compilation dominates (marginal difference)
Key insight: Even when performance gains are modest, type safety alone justifies using generic variants.
Real Benchmark Results
ElementsMatch: The Star Performer
Comparison Operations
Equality Checks
Why These Numbers Matter
1. Allocation Elimination
The most dramatic speedups come from eliminating allocations entirely:
- ElementsMatch: 501,503 β 3 allocations (1000 elements)
- All comparisons: 1 β 0 allocations
- Ordering checks: 4-11 β 0 allocations
Less allocation pressure means faster execution and reduced GC overhead.
2. Superlinear Scaling
ElementsMatch’s O(nΒ²) complexity amplifies the benefits:
- 10 elements: 21x faster
- 100 elements: 39x faster
- 1000 elements: 81x faster
The speedup increases with collection size.
3. Cumulative Impact
If your test suite uses assertions thousands of times:
- 10x speedup per assertion = significantly faster test runs
- Especially impactful in CI/CD pipelines
Migration Guide
Step 1: Identify Generic-Capable Assertions
Look for these common assertions in your tests:
- Equal, NotEqual β EqualT, NotEqualT
- Greater, Less, Positive, Negative β GreaterT, LessT, PositiveT, NegativeT
- Contains, ElementsMatch, Subset β ContainsT, ElementsMatchT, SubsetT
- IsIncreasing, IsDecreasing β IsIncreasingT, IsDecreasingT
- IsOfType β IsOfTypeT (eliminates need for dummy values!)
Step 2: Add Type Parameters
Step 3: Fix Type Mismatches
The compiler will now catch type errors:
Conclusion
Generic assertions deliver two major benefits:
Type Safety (Primary Goal): Catch errors when writing tests
- Compiler catches type mismatches immediately
- IDE autocomplete guides to correct types
- Refactoring safety: broken tests identified at compile time
Performance (Unexpected Bonus): 1.2-81x faster
- Zero allocation overhead for most operations
- Dramatic gains for collection operations
- Cumulative benefits across large test suites
Recommendation: Prefer generic variants (*T functions) wherever available. The type safety alone justifies the switch; the performance improvement is a bonus.
The Bottom Line
The performance improvements validate the design choice, but type safety was always the goal.
Running Your Own Benchmarks
Coverage
38 generic functions benchmarked across 10 domains:
- Boolean (2): TrueT, FalseT
- Collection (12): StringContainsT, SliceContainsT, MapContainsT, SeqContainsT, ElementsMatchT, SliceSubsetT, and negative variants
- Comparison (6): GreaterT, LessT, GreaterOrEqualT, LessOrEqualT, PositiveT, NegativeT
- Equality (4): EqualT, NotEqualT, SameT, NotSameT
- JSON (1): JSONEqT
- Number (2): InDeltaT, InEpsilonT
- Ordering (6): IsIncreasingT, IsDecreasingT, IsNonIncreasingT, IsNonDecreasingT, SortedT, NotSortedT
- String (2): RegexpT, NotRegexpT
- Type (2): IsOfTypeT, IsNotOfTypeT
- YAML (1): YAMLEqT (benchmarked separately in enable/yaml module)