背景:为什么 Redis 需要更快的浮点解析?
Redis 的开发中,高性能一直是核心追求之一。特别是在处理用户输入的浮点数字符串时,传统的标准库解析函数 strtod()
成为性能瓶颈。于是,Redis 引入了 Daniel Lemire 等人开发的 fast_float 库,以实现更快的浮点数解析
Redis 的很多命令,如 ZADD
(Sorted Set 添加成员)、INCRBYFLOAT
(浮点数增量)等,都需要把用户传入的字符串浮点数转换成机器内部的 double
类型
传统方法的问题
- 大多数 C/C++ 程序使用标准库的
strtod()
来完成字符串到浮点数的转换 strtod()
实现非常复杂,旨在处理各种边界情况、遵守 locale 规则、支持特殊浮点格式- 这些特性导致
strtod()
运行缓慢,成为 Redis 解析性能的瓶颈,尤其是在高并发场景
因此,Redis 团队决定引入性能更优的第三方解析库,以降低命令执行延迟和 CPU 占用
fast_float:论文级优化的浮点数解析库
fast_float
由 Daniel Lemire 等人设计,旨在显著提升浮点数解析性能。它的核心设计基于如下论文:
- 论文标题: Parsing billions of floating point numbers per second
- 作者: Daniel Lemire et al.
- 链接:https://arxiv.org/abs/2101.11408
核心优化思想
1. 跳过传统 strtod()
的复杂实现
strtod()
为了兼容性和正确性,包含大量分支、locale 检查、无关字符跳过和高精度舍入计算
fast_float
直接面向“常见且合法”的输入,抛弃 locale 等复杂处理,大幅简化流程
2. 直接十进制 → 二进制浮点转换
传统方案通常将输入字符串先转成高精度十进制数(如大整数),再通过复杂算法转换为 IEEE 754 浮点数
fast_float
通过数学方法直接将十进制有效数字和指数转换为二进制浮点,避免了中间的高精度十进制大整数运算
简言之,就是:
- 解析字符串提取 有效数字(mantissa)和 指数(exponent)
- 用预计算的指数乘数表对浮点数做快速缩放
- 通过位运算直接构造 IEEE 754 二进制格式
3. 极少分支且分离快慢路径
为提高 CPU 分支预测命中率,fast_float
设计了极简分支逻辑。绝大多数合法输入走最快路径,边缘异常输入才走慢路径
4. SIMD 指令优化
fast_float
利用现代 CPU 的 SIMD 指令(如 SSE2、AVX2)批量扫描字符,快速判断数字和小数点,加速解析
性能测试对比
测试设计
- 选用一组典型浮点数字符串,如
"3.14159"
,"2.7182818284"
,"1.0e10"
,"6.022e23"
等 - 分别用标准库
strtod()
和fast_float::from_chars()
解析同一批字符串 - 进行多次迭代,测量总耗时
- 避免计算结果溢出,使用
volatile
变量防止编译器优化
fast_float_test.cpp
#include <iostream>
#include <vector>
#include <chrono>
#include <cstring>
#include "fast_float/fast_float.h"
const char* test_strings[] = {
"3.14159", "2.7182818284", "1.0e10", "6.022e23",
"2.2250738585072014e-308",
"0.0", "123456789.987654321", "9.999999999999999e-10", "1e-10"
};
constexpr size_t NUM_STRINGS = std::size(test_strings);
constexpr int ITERATIONS = 1000000;
volatile double sink = 0.0; // 防止优化
int main(){
// 测试 strtod 性能
{
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < ITERATIONS; ++i){
for (size_t j = 0; j < NUM_STRINGS; ++j){
double val = strtod(test_strings[j], nullptr);
sink += val * 0; // 乘0避免溢出,但又用到 val 防止优化
}
}
auto end = std::chrono::high_resolution_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << "strtod total time: " << ms << " ms\n";
}
// 测试 fast_float 性能
{
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < ITERATIONS; ++i){
for (size_t j = 0; j < NUM_STRINGS; ++j){
double val;
auto res = fast_float::from_chars(test_strings[j], test_strings[j] + std::strlen(test_strings[j]), val);
if (res.ec == std::errc()){
sink += val * 0; // 同上,防止优化
}
}
}
auto end = std::chrono::high_resolution_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << "fast_float total time: " << ms << " ms\n";
}
return 0;
}
g++ -std=c++20 -O3 -march=native fast_float_test.cpp
./a.out
strtod total time: 404 ms
fast_float total time: 76 ms
-march=native
会启用所有当前 CPU 支持的指令集
总结
方面 | strtod() | fast_float |
---|---|---|
设计目标 | 兼容、正确性全面 | 高性能、常见输入快速路径 |
处理逻辑 | 多分支、复杂 | 直接十进制→二进制,分支少 |
性能 | 较慢 | 约 5~10 倍加速 |
SIMD支持 | 无 | 有,利用现代 CPU 指令 |
go版本
package main
import (
"fmt"
"strconv"
"time"
)
// 示例浮点数字符串数组
var testStrings = []string{
"3.14159", "2.7182818284", "1.0e10", "6.022e23",
"2.2250738585072014e-308", "0.0", "123456789.987654321", "9.999999999999999e-10", "1e-10",
}
const iterations = 1000000
// 使用 strconv.ParseFloat 进行浮点数解析的测试函数
func testParseFloat() float64 {
var sum float64
for i := 0; i < iterations; i++ {
for _, str := range testStrings {
val, err := strconv.ParseFloat(str, 64)
if err != nil {
fmt.Println("Error parsing float:", err)
return 0
}
// 累加结果,防止编译器优化
sum += val
}
}
return sum
}
func main() {
// 测试 ParseFloat 性能
start := time.Now()
testParseFloat()
duration := time.Since(start)
// 输出总耗时
fmt.Printf("strconv.ParseFloat total time: %v\n", duration)
}
strconv.ParseFloat total time: 189.067ms
java版本
import java.util.concurrent.TimeUnit;
public class Main {
// 示例浮点数字符串数组
static String[] testStrings = {
"3.14159", "2.7182818284", "1.0e10", "6.022e23", "2.2250738585072014e-308",
"0.0", "123456789.987654321", "9.999999999999999e-10", "1e-10"
};
static final int ITERATIONS = 1000000;
public static void main(String[] args) {
// 测试性能
long start = System.nanoTime();
testParseFloat();
long duration = System.nanoTime() - start;
// 输出总耗时
System.out.printf("total time: %d ms\n", TimeUnit.NANOSECONDS.toMillis(duration));
}
// 使用 Double.parseDouble 进行浮点数解析的测试函数
public static double testParseFloat() {
double sum = 0.0;
for (int i = 0; i < ITERATIONS; i++) {
for (String str : testStrings) {
try {
double val = Double.parseDouble(str);
// 累加结果,防止编译器优化
sum += val;
} catch (NumberFormatException e) {
System.out.println("Error parsing float: " + e.getMessage());
return 0;
}
}
}
return sum;
}
}
total time: 505 ms
rust版
use std::time::Instant;
use rayon::prelude::*;
static TEST_STRINGS: [&str; 9] = [
"3.14159", "2.7182818284", "1.0e10", "6.022e23",
"2.2250738585072014e-308", "0.0", "123456789.987654321", "9.999999999999999e-10", "1e-10",
];
const ITERATIONS: usize = 1_000_000;
fn main() {
// 运行串行性能测试并记录时间
let start = Instant::now();
test_parse_float(); // 运行 test_parse_float
let duration = start.elapsed();
println!("f64::from_str total time (serial): {} ms", duration.as_millis());
// 运行并行性能测试并记录时间
let start_parallel = Instant::now();
test_parse_float_parallel(); // 运行 test_parse_float_parallel
let duration_parallel = start_parallel.elapsed();
println!("f64::from_str total time (parallel): {} ms", duration_parallel.as_millis());
}
fn test_parse_float_parallel() -> f64 {
(0..ITERATIONS) // ITERATIONS 次数
.into_par_iter() // 使用并行迭代器
.map(|_| {
TEST_STRINGS
.par_iter()
.map(|&s| s.parse::<f64>().unwrap_or(0.0)) // 并行解析每个字符串
.sum::<f64>() // 每个测试字符串解析后的累加值
})
.sum() // 将所有 ITERATIONS 的结果求和
}
fn test_parse_float() -> f64 {
let mut sum = 0.0;
for _ in 0..ITERATIONS {
for &s in TEST_STRINGS.iter() {
match s.parse::<f64>() {
Ok(val) => sum += val,
Err(_) => {
println!("Error parsing float");
return 0.0;
}
}
}
}
sum
}
f64::from_str total time (serial): 85 ms
f64::from_str total time (parallel): 25 ms