需要一种快速的方法将大量的双精度转换为字符串

Need a fast method to convert large amount of double to string

我正在为高速计算程序编写结果输出模块。

我的计划是:

  1. 我的任务是以相对较快的速度将结果插入数据库(PostgreSQL)。
  2. 我使用 libpq 的 [COPY FROM STDIN],有人告诉我这是最快的方法。
  3. 该方法需要将结果转换成char*格式。

虽然结果看起来像这样:

  1. 未来 106 年的每月现金流量(总共 1272 倍)。
  2. 每个条目大约有 14 个现金流量。
  3. 大约 2800 个实体(测试数据为 2790 个)。

数据库中的 table 如下所示:

  1. table 的每一行包含一个实体。
  2. 有一些前缀来标识不同的实体。
  3. CashFlows 是前缀后的双数组(PGSQL 中的 float8[] 类型)。

下面给出了在数据库中创建table的代码:

create table AgentCF(
PlanID     int4,
Agent      int4,
Senario    int4,
RM_Prev    float8[], DrvFac_Cur float8[], Prem       float8[],
Comm       float8[], CommOR     float8[], FixExp     float8[],
VarExp     float8[], CIRCFee    float8[], SaftyFund  float8[],
Surr       float8[], Benefit_1  float8[], Benefit_2  float8[],
Benefit_3  float8[], Benefit_4  float8[], Benefit_5  float8[],
Benefit_6  float8[], Benefit_7  float8[], Benefit_8  float8[],
Benefit_9  float8[], Benefit_10 float8[]
);

正在为准备插入的 CashFlow 的函数提供代码:

void AsmbCF(char *buffer, int size, int ProdNo, int i, int Pos, int LineEnd)
{
    int     j, Step = sizeof(nodecf) / sizeof(double), PosST, Temp;
    double *LoopRate = &AllHeap[ProdNo].Heap.AgentRes[i].CF.NodeCF[0].Prem;
    strcpy_s(buffer, size, "{");
    for (j = 0; j < TOTLEN / 10; j++) {
        PosST = j * 10 * Step + Pos;
        sprintf_s(&buffer[strlen(buffer)], size - strlen(buffer), "%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,",
            LoopRate[PosST],
            LoopRate[PosST + 1 * Step],
            LoopRate[PosST + 2 * Step],
            LoopRate[PosST + 3 * Step],
            LoopRate[PosST + 4 * Step],
            LoopRate[PosST + 5 * Step],
            LoopRate[PosST + 6 * Step],
            LoopRate[PosST + 7 * Step],
            LoopRate[PosST + 8 * Step],
            LoopRate[PosST + 9 * Step]
        );
    }
    Temp = j * 10;
    PosST = Temp * Step + Pos;
    sprintf_s(&buffer[strlen(buffer)], size - strlen(buffer), "%f", LoopRate[PosST]);
    Temp = Temp + 1;
    for (j = Temp; j < TOTLEN; j++) {
        PosST = j * Step + Pos;
        sprintf_s(&buffer[strlen(buffer)], size - strlen(buffer), ",%f", LoopRate[PosST]);
    }
    if (LineEnd) {
        strcat_s(buffer, size, "}\n");
    }
    else {
        strcat_s(buffer, size, "}\t");
    }
}

以下为速度测试代码:

void ThreadOutP(LPVOID pM)
{
    char       *buffer = malloc(BUFFLEN), sql[SQLLEN];
    int         Status, ProdNo = (int)pM, i, j, ben;
    PGconn     *conn = NULL;
    PGresult   *res;
    clock_t     begin, end;

    fprintf_s(fpOutP, "PlanID %d Start inseting...\n", AllHeap[ProdNo].PlanID);
    begin = clock();
    DBConn(&conn, CONNSTR, fpOutP);

#pragma region General cashflow
    //============================== Data Query ==============================
    //strcpy_s(&sql[0], SQLLEN, "COPY AgentCF(PlanID,Agent,Senario,Prem,Comm,CommOR,CIRCFee,SaftyFund,FixExp,VarExp,Surr");
    //for (ben = 1; ben <= AllHeap[ProdNo].Heap.TotNo.NoBenft; ben++) {
    //  strcat_s(&sql[0], SQLLEN, ",Benefit_");
    //  _itoa_s(ben, &sql[strlen(sql)], sizeof(sql) - strlen(sql), 10);
    //}
    //strcat_s(&sql[0], SQLLEN, ") FROM STDIN;");
    //res = PQexec(conn, &sql[0]);
    //if (PQresultStatus(res) != PGRES_COPY_IN) {
    //  fprintf_s(fpOutP, "Not in COPY_IN mode\n");
    //}
    //PQclear(res);
    //============================== Data Apply ==============================
    for (i = 0; i < AllHeap[ProdNo].MaxAgntPos + AllHeap[ProdNo].Heap.TotNo.NoSensi; i++) {
        sprintf_s(buffer, BUFFLEN, "%d\t%d\t%d\t", AllHeap[ProdNo].PlanID, AllHeap[ProdNo].Heap.AgentRes[i].Agent, AllHeap[ProdNo].Heap.AgentRes[i].Sensi);
        //Status = PQputCopyData(conn, buffer, (int)strlen(buffer));
        //if (1 != Status) {
        //  fprintf_s(fpOutP, "PlanID %d inserting error for agent %d\n", AllHeap[ProdNo].PlanID, AllHeap[ProdNo].Heap.AgentRes[i].Agent);
        //}
        for (j = 0; j < 8 + AllHeap[ProdNo].Heap.TotNo.NoBenft; j++) {
            if (j == 7 + AllHeap[ProdNo].Heap.TotNo.NoBenft) {
                AsmbCF(buffer, BUFFLEN, ProdNo, i, j, 1);
            }
            else {
                AsmbCF(buffer, BUFFLEN, ProdNo, i, j, 0);
            }
            //Status = PQputCopyData(conn, buffer, (int)strlen(buffer));
            //if (1 != Status) {
            //  fprintf_s(fpOutP, "PlanID %d inserting error for agent %d\n", AllHeap[ProdNo].PlanID, AllHeap[ProdNo].Heap.AgentRes[i].Agent);
            //}
        }
    }
    //Status = PQputCopyEnd(conn, NULL);
#pragma endregion

#pragma region K cashflow

#pragma endregion

    PQfinish(conn);
    FreeProd(ProdNo);
    free(buffer);
    end = clock();
    fprintf_s(fpOutP, "PlanID %d inserted, total %d rows inserted, %d millisecond cost\n", AllHeap[ProdNo].PlanID, i, end - begin);
    AllHeap[ProdNo].Printed = 1;
}

请注意,我禁用了涉及插入的代码。

测试结果为:

  1. 仅组装字符串的成本是 45930 毫秒。
  2. 组装字符串和插入的成本是 54829 毫秒。

所以大部分成本在于将 double 转换为 char。

所以想请问有没有更快的方法将double系列转成string,因为相比计算成本,瓶颈其实是结果的输出。

顺便说一句,我的平台是 Windows 10,PostgreSQL 11,Visual Studio 2017.

非常感谢!

fast method to convert large amount of double to string

对于完整的 double 范围应用,请使用 sprintf(buf, "%a", some_double)。如果需要十进制输出,请使用 "%e".

任何其他代码只有在包含准确性或允许的输入范围时才会更快不知何故

通常 方法是将double x 转换为某个宽标度整数并将其转换为字符串。这意味着对 x 的限制尚未由 OP 明确表达。

即使其他一些方法看起来更快,但随着代码的发展或移植,它可能不会更快。


OP 需要 post 的是用于 objective 性能评估的速度测试代码。

实际上有几种更快的方法可以将浮点数准确地表示为字符串,其中之一是 Grisu, by Florian Loitsch

This github repo compares several algorithms in C and C++, and it contains the source code for the Grisu2 method in C,他声称比 sprintf.

快 5.7 倍

但是,同一个 repo (Milo Yip) 的作者提供了他自己的 C++ 单一 header 实现,据称速度提高了 9.1 倍,大概是因为更多的函数是完全内联的。我相信将此代码移植到 C 应该是微不足道的,因为它不使用任何特殊的 C++ 语法。

替代 chux 的回答,我做了以下功能:

__inline char* dbltoa(char* buff, double A, int Precision)
{
    int     Temp;
    char   *ptr;

    Temp = (int)A;
    _itoa_s(Temp, buff, 50, 10);
    ptr = buff + strlen(buff);
    ptr[0] = '.';
    Temp = (int)((A - Temp) * pow(10, Precision));
    _itoa_s(Temp, ptr + 1, 50, 10);
    return ptr + strlen(ptr);
}

并更新了生成 CashFlow 字符串的函数:

void AsmbCF(char *buffer, int size, int ProdNo, int i, int Pos, int LineEnd)
{
    int     j, Step = sizeof(nodecf) / sizeof(double), PosST, Temp;
    double *LoopRate = &AllHeap[ProdNo].Heap.AgentRes[i].CF.NodeCF[0].Prem;
    char   *ptr;
    strcpy_s(buffer, size, "{");
    ptr = buffer + 1;
    for (j = 0; j < TOTLEN; j++) {
        PosST = j * Step + Pos;
        ptr = dbltoa(ptr, LoopRate[PosST], 8);
        ptr[0] = ',';
        ptr++;
    }
    ptr[-1] = 0;
    if (LineEnd) {
        strcat_s(buffer, size, "}\n");
    }
    else {
        strcat_s(buffer, size, "}\t");
    }
}

没有插入的测试结果是4558毫秒,而插入需要29260毫秒(可能是数据库的并行运行使得这个按比例不相等)。

我对原始代码做了一些记账:


  Total score("function" calls):
    2 + 4*TOTLEN * strlen()
    1 + 2*TOTLEN * sprintf() 
    1 * strcat()

  Estimated string() cost:
    3 + 4* size * (TOTLEN*TOTLEN) / 2 (measured in characters)

  Estimated sprintf() cost:
    2 * TOTLEN (measured in %lf conversions)
    2 * size (measured in characters)

现在,我不知道 TOTLEN 是什么,但是在不断增长的字符串上调用 strlen() 和朋友会导致二次行为,请参阅 https://en.wikipedia.org/wiki/Joel_Spolsky#Schlemiel_the_Painter.27s_algorithm


  • profile/measure(或思考)在优化之前
  • snprintf() 正确使用时是溢出安全的;阅读手册页并使用 return 值
  • strxxx_x() 功能几乎没用,它们的存在只是为了取悦 PHB