背景:为什么 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