Hello people! It's been a while...
I've applied some very long-overdue TLC to this library and have some great news for anybody using this in the real-world. The old performance was so embarrassingly bad I'm surprised it was even usable. The new version very much is production-ready and much, much quicker.
v1.2.0 brings significant performance improvements and greatly reduced memory usage, along with a set of alloc-friendly Decoder/Encoder APIs, allowing efficient reuse of buffers for large batch jobs or long-running services.
Performance Highlights
| Operation | Improvement |
|---|---|
| Encode | ~50-140x faster (41ms → 829µs and 75ms → 534µs) |
| Decode | ~11x faster (332µs → 30µs) |
| Encode memory | 99.4% reduction (3MB → 14KB) |
| Encode allocations | 500,000 → 6 per operation |
Reusable Encoder/Decoder APIs
New Encoder and Decoder types allow reusing internal buffers across multiple operations, allowing for near zero-alloc (amortized) batch processing:
enc := blurhash.NewEncoder()
for _, img := range images {
hash, _ := enc.Encode(3, 4, img)
}
dec := blurhash.NewDecoder()
for _, hash := range hashes {
img, _ := dec.Decode(hash, 128, 128, 1)
}Other Changes
- A little modernization/cleanup for now widely available Go features
- Removed the only dependency this library had, which was test-only anyway
- Added fuzz testing (and found a few bugs with it!)
Benchmarks
benchstat v1.1.1...v1.2.0 (click to expand)
goos: linux
goarch: amd64
pkg: github.com/bbrks/go-blurhash
cpu: AMD Ryzen 7 7800X3D 8-Core Processor
│ bench-v1.1.1.txt │ bench-v1.2.0.txt │
│ sec/op │ sec/op vs base │
Components/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 8.511n ± 1% 5.570n ± 1% -34.56% (p=0.000 n=10)
Components/LNAdApj[00aymkj[TKay9}ay-Sj[-16 8.425n ± 1% 5.507n ± 2% -34.63% (p=0.000 n=10)
Components/LNMF%n00%#MwS|WCWEM{R*bbWBbH-16 8.418n ± 1% 5.485n ± 2% -34.85% (p=0.000 n=10)
Components/KJG8_@Dgx]_4V?xuyE%NRj-16 8.406n ± 1% 5.557n ± 2% -33.90% (p=0.000 n=10)
Decode/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 332.56µ ± 1% 29.69µ ± 1% -91.07% (p=0.000 n=10)
Decode/LNAdApj[00aymkj[TKay9}ay-Sj[-16 301.43µ ± 1% 30.25µ ± 1% -89.97% (p=0.000 n=10)
Decode/LNMF%n00%#MwS|WCWEM{R*bbWBbH-16 332.42µ ± 1% 29.73µ ± 1% -91.06% (p=0.000 n=10)
Decode/KJG8_@Dgx]_4V?xuyE%NRj-16 288.01µ ± 0% 24.39µ ± 1% -91.53% (p=0.000 n=10)
DecodeDraw/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 329.46µ ± 1% 28.56µ ± 1% -91.33% (p=0.000 n=10)
DecodeDraw/LNAdApj[00aymkj[TKay9}ay-Sj[-16 300.15µ ± 0% 28.89µ ± 0% -90.37% (p=0.000 n=10)
DecodeDraw/LNMF%n00%#MwS|WCWEM{R*bbWBbH-16 332.43µ ± 1% 28.56µ ± 2% -91.41% (p=0.000 n=10)
DecodeDraw/KJG8_@Dgx]_4V?xuyE%NRj-16 287.61µ ± 1% 23.24µ ± 1% -91.92% (p=0.000 n=10)
Encode/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 75498.7µ ± 1% 534.2µ ± 1% -99.29% (p=0.000 n=10)
Encode/LNAdApj[00aymkj[TKay9}ay-Sj[-16 41166.9µ ± 1% 829.3µ ± 3% -97.99% (p=0.000 n=10)
Components/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 5.570n ± 2%
Decode/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 55.47µ ± 1%
DecodeDraw/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 54.14µ ± 1%
DecoderReuse/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 29.05µ ± 1%
DecoderReuse/LNAdApj[00aymkj[TKay9}ay-Sj[-16 29.42µ ± 1%
DecoderReuse/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 54.47µ ± 1%
DecoderReuse/LNMF%n00%#MwS|WCWEM{R*bbWBbH-16 28.90µ ± 1%
DecoderReuse/KJG8_@Dgx]_4V?xuyE%NRj-16 23.86µ ± 1%
DecoderDrawReuse/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 27.80µ ± 2%
DecoderDrawReuse/LNAdApj[00aymkj[TKay9}ay-Sj[-16 27.96µ ± 1%
DecoderDrawReuse/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 52.58µ ± 1%
DecoderDrawReuse/LNMF%n00%#MwS|WCWEM{R*bbWBbH-16 27.73µ ± 1%
DecoderDrawReuse/KJG8_@Dgx]_4V?xuyE%NRj-16 22.19µ ± 1%
Encode/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 26.79m ± 4%
EncoderReuse/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 536.6µ ± 1%
EncoderReuse/LNAdApj[00aymkj[TKay9}ay-Sj[-16 814.4µ ± 3%
EncoderReuse/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 26.87m ± 3%
geomean 32.44µ 17.85µ -88.19%
│ bench-v1.1.1.txt │ bench-v1.2.0.txt │
│ B/op │ B/op vs base │
Components/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Components/LNAdApj[00aymkj[TKay9}ay-Sj[-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Components/LNMF%n00%#MwS|WCWEM{R*bbWBbH-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Components/KJG8_@Dgx]_4V?xuyE%NRj-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Decode/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 4.344Ki ± 0% 6.094Ki ± 0% +40.29% (p=0.000 n=10)
Decode/LNAdApj[00aymkj[TKay9}ay-Sj[-16 4.344Ki ± 0% 6.094Ki ± 0% +40.29% (p=0.000 n=10)
Decode/LNMF%n00%#MwS|WCWEM{R*bbWBbH-16 4.344Ki ± 0% 6.094Ki ± 0% +40.29% (p=0.000 n=10)
Decode/KJG8_@Dgx]_4V?xuyE%NRj-16 4.281Ki ± 0% 5.781Ki ± 0% +35.04% (p=0.000 n=10)
DecodeDraw/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 4.344Ki ± 0% 2.031Ki ± 0% -53.24% (p=0.000 n=10)
DecodeDraw/LNAdApj[00aymkj[TKay9}ay-Sj[-16 4.344Ki ± 0% 2.031Ki ± 0% -53.24% (p=0.000 n=10)
DecodeDraw/LNMF%n00%#MwS|WCWEM{R*bbWBbH-16 4.344Ki ± 0% 2.031Ki ± 0% -53.24% (p=0.000 n=10)
DecodeDraw/KJG8_@Dgx]_4V?xuyE%NRj-16 4.281Ki ± 0% 1.719Ki ± 0% -59.85% (p=0.000 n=10)
Encode/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 1951.34Ki ± 0% 11.96Ki ± 0% -99.39% (p=0.000 n=10)
Encode/LNAdApj[00aymkj[TKay9}ay-Sj[-16 3072.60Ki ± 0% 14.34Ki ± 0% -99.53% (p=0.000 n=10)
Components/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 0.000 ± 0%
Decode/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 7.188Ki ± 0%
DecodeDraw/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 3.125Ki ± 0%
DecoderReuse/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 4.062Ki ± 0%
DecoderReuse/LNAdApj[00aymkj[TKay9}ay-Sj[-16 4.062Ki ± 0%
DecoderReuse/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 4.062Ki ± 0%
DecoderReuse/LNMF%n00%#MwS|WCWEM{R*bbWBbH-16 4.062Ki ± 0%
DecoderReuse/KJG8_@Dgx]_4V?xuyE%NRj-16 4.062Ki ± 0%
DecoderDrawReuse/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 0.000 ± 0%
DecoderDrawReuse/LNAdApj[00aymkj[TKay9}ay-Sj[-16 0.000 ± 0%
DecoderDrawReuse/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 0.000 ± 0%
DecoderDrawReuse/LNMF%n00%#MwS|WCWEM{R*bbWBbH-16 0.000 ± 0%
DecoderDrawReuse/KJG8_@Dgx]_4V?xuyE%NRj-16 0.000 ± 0%
Encode/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 80.74Ki ± 0%
EncoderReuse/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 61.00 ± 0%
EncoderReuse/LNAdApj[00aymkj[TKay9}ay-Sj[-16 66.00 ± 2%
EncoderReuse/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 1.992Ki ± 7%
geomean ² -58.58% ²
¹ all samples are equal
² summaries must be >0 to compute geomean
│ bench-v1.1.1.txt │ bench-v1.2.0.txt │
│ allocs/op │ allocs/op vs base │
Components/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Components/LNAdApj[00aymkj[TKay9}ay-Sj[-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Components/LNMF%n00%#MwS|WCWEM{R*bbWBbH-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Components/KJG8_@Dgx]_4V?xuyE%NRj-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Decode/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 3.000 ± 0% 5.000 ± 0% +66.67% (p=0.000 n=10)
Decode/LNAdApj[00aymkj[TKay9}ay-Sj[-16 3.000 ± 0% 5.000 ± 0% +66.67% (p=0.000 n=10)
Decode/LNMF%n00%#MwS|WCWEM{R*bbWBbH-16 3.000 ± 0% 5.000 ± 0% +66.67% (p=0.000 n=10)
Decode/KJG8_@Dgx]_4V?xuyE%NRj-16 3.000 ± 0% 5.000 ± 0% +66.67% (p=0.000 n=10)
DecodeDraw/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=10) ¹
DecodeDraw/LNAdApj[00aymkj[TKay9}ay-Sj[-16 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=10) ¹
DecodeDraw/LNMF%n00%#MwS|WCWEM{R*bbWBbH-16 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=10) ¹
DecodeDraw/KJG8_@Dgx]_4V?xuyE%NRj-16 3.000 ± 0% 3.000 ± 0% ~ (p=1.000 n=10) ¹
Encode/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 499441.000 ± 0% 6.000 ± 0% -100.00% (p=0.000 n=10)
Encode/LNAdApj[00aymkj[TKay9}ay-Sj[-16 786481.000 ± 0% 6.000 ± 0% -100.00% (p=0.000 n=10)
Components/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 0.000 ± 0%
Decode/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 5.000 ± 0%
DecodeDraw/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 3.000 ± 0%
DecoderReuse/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 2.000 ± 0%
DecoderReuse/LNAdApj[00aymkj[TKay9}ay-Sj[-16 2.000 ± 0%
DecoderReuse/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 2.000 ± 0%
DecoderReuse/LNMF%n00%#MwS|WCWEM{R*bbWBbH-16 2.000 ± 0%
DecoderReuse/KJG8_@Dgx]_4V?xuyE%NRj-16 2.000 ± 0%
DecoderDrawReuse/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 0.000 ± 0%
DecoderDrawReuse/LNAdApj[00aymkj[TKay9}ay-Sj[-16 0.000 ± 0%
DecoderDrawReuse/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 0.000 ± 0%
DecoderDrawReuse/LNMF%n00%#MwS|WCWEM{R*bbWBbH-16 0.000 ± 0%
DecoderDrawReuse/KJG8_@Dgx]_4V?xuyE%NRj-16 0.000 ± 0%
Encode/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 7.000 ± 0%
EncoderReuse/LFE.@D9F01_2%L%MIVD*9Goe-;WB-16 3.000 ± 0%
EncoderReuse/LNAdApj[00aymkj[TKay9}ay-Sj[-16 3.000 ± 0%
EncoderReuse/eaF#5R0#WBjYR+58-nWCWBn~bIsTbbayjFWof8jFj[WX-nNHR*jss.-16 4.000 ± 0%
geomean ² -77.80% ²
¹ all samples are equal
² summaries must be >0 to compute geomean
pkg: github.com/bbrks/go-blurhash/base83
│ bench-v1.1.1.txt │ bench-v1.2.0.txt │
│ sec/op │ sec/op vs base │
Decode/3-16 8.936n ± 1% 2.064n ± 1% -76.90% (p=0.000 n=10)
Decode/A-16 9.007n ± 2% 2.060n ± 2% -77.13% (p=0.000 n=10)
Decode/:-16 8.999n ± 1% 2.072n ± 1% -76.97% (p=0.000 n=10)
Decode/~-16 8.901n ± 0% 2.064n ± 2% -76.81% (p=0.000 n=10)
Decode/01-16 8.889n ± 1% 2.060n ± 1% -76.83% (p=0.000 n=10)
Decode/11-16 8.933n ± 1% 2.077n ± 1% -76.76% (p=0.000 n=10)
Decode/33-16 8.921n ± 1% 2.083n ± 1% -76.65% (p=0.000 n=10)
Decode/~$-16 8.838n ± 1% 2.096n ± 2% -76.29% (p=0.000 n=10)
Decode/%%%%%%-16 8.835n ± 1% 2.072n ± 1% -76.54% (p=0.000 n=10)
Encode/3-16 36.710n ± 1% 4.140n ± 4% -88.72% (p=0.000 n=10)
Encode/A-16 37.020n ± 1% 4.200n ± 3% -88.65% (p=0.000 n=10)
Encode/:-16 36.735n ± 1% 4.190n ± 4% -88.59% (p=0.000 n=10)
Encode/~-16 36.975n ± 0% 4.124n ± 3% -88.85% (p=0.000 n=10)
Encode/01-16 37.585n ± 2% 4.286n ± 4% -88.60% (p=0.000 n=10)
Encode/11-16 37.055n ± 2% 4.214n ± 3% -88.63% (p=0.000 n=10)
Encode/33-16 37.295n ± 1% 4.223n ± 3% -88.68% (p=0.000 n=10)
Encode/~$-16 37.275n ± 1% 4.117n ± 3% -88.96% (p=0.000 n=10)
Encode/%%%%%%-16 37.395n ± 1% 4.263n ± 1% -88.60% (p=0.000 n=10)
geomean 18.19n 2.948n -83.79%
│ bench-v1.1.1.txt │ bench-v1.2.0.txt │
│ B/op │ B/op vs base │
Decode/3-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Decode/A-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Decode/:-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Decode/~-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Decode/01-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Decode/11-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Decode/33-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Decode/~$-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Decode/%%%%%%-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Encode/3-16 12.00 ± 0% 0.00 ± 0% -100.00% (p=0.000 n=10)
Encode/A-16 12.00 ± 0% 0.00 ± 0% -100.00% (p=0.000 n=10)
Encode/:-16 12.00 ± 0% 0.00 ± 0% -100.00% (p=0.000 n=10)
Encode/~-16 12.00 ± 0% 0.00 ± 0% -100.00% (p=0.000 n=10)
Encode/01-16 12.00 ± 0% 0.00 ± 0% -100.00% (p=0.000 n=10)
Encode/11-16 12.00 ± 0% 0.00 ± 0% -100.00% (p=0.000 n=10)
Encode/33-16 12.00 ± 0% 0.00 ± 0% -100.00% (p=0.000 n=10)
Encode/~$-16 12.00 ± 0% 0.00 ± 0% -100.00% (p=0.000 n=10)
Encode/%%%%%%-16 12.00 ± 0% 0.00 ± 0% -100.00% (p=0.000 n=10)
geomean ² ? ² ³
¹ all samples are equal
² summaries must be >0 to compute geomean
³ ratios must be >0 to compute geomean
│ bench-v1.1.1.txt │ bench-v1.2.0.txt │
│ allocs/op │ allocs/op vs base │
Decode/3-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Decode/A-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Decode/:-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Decode/~-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Decode/01-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Decode/11-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Decode/33-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Decode/~$-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Decode/%%%%%%-16 0.000 ± 0% 0.000 ± 0% ~ (p=1.000 n=10) ¹
Encode/3-16 3.000 ± 0% 0.000 ± 0% -100.00% (p=0.000 n=10)
Encode/A-16 3.000 ± 0% 0.000 ± 0% -100.00% (p=0.000 n=10)
Encode/:-16 3.000 ± 0% 0.000 ± 0% -100.00% (p=0.000 n=10)
Encode/~-16 3.000 ± 0% 0.000 ± 0% -100.00% (p=0.000 n=10)
Encode/01-16 3.000 ± 0% 0.000 ± 0% -100.00% (p=0.000 n=10)
Encode/11-16 3.000 ± 0% 0.000 ± 0% -100.00% (p=0.000 n=10)
Encode/33-16 3.000 ± 0% 0.000 ± 0% -100.00% (p=0.000 n=10)
Encode/~$-16 3.000 ± 0% 0.000 ± 0% -100.00% (p=0.000 n=10)
Encode/%%%%%%-16 3.000 ± 0% 0.000 ± 0% -100.00% (p=0.000 n=10)
geomean ² ? ² ³
¹ all samples are equal
² summaries must be >0 to compute geomean
³ ratios must be >0 to compute geomean
Full Changelog: v1.1.1...v1.2.0