리눅스 백엔드 서비스 문자열 데이터 처리 - 속도가 느림

C++ 개발의 과거 프로젝트에서 우리는 자체 프로토콜을 사용하여 통신했고, 이 프로토콜은 2차원 배열 패턴을 채택했습니다. 대량의 데이터를 처리할 때 프로토콜 내부에서 배열을 순회하고 시리얼화 작업을 수행하여 로그를 생성해야 했는데, 효율성이 낮아 시스템이 고부하 상태에서 눈에 띄는 끊김 현상을 일으켰고, 이로 인해 사업 부서로부터 시스템 끊김에 대한 피드백을 받았습니다.

문제 파악

문제 해결 과정에서, 우리는 먼저 시스템 성능을 분석했고, 대량의 데이터를 처리할 때 CPU 사용률이 눈에 띄게 증가하고 시스템 응답 시간이 길어지는 것을 확인했습니다. 시스템 로그를 분석한 결과, 많은 양의 직렬화 작업이 있었고, 이러한 작업은 특히 2차원 배열을 처리할 때 효율성이 낮아 시스템 성능 저하를 유발하는 것으로 나타났습니다.

pstack 도구로 스레드 정보를 캡처한 결과, 로그 스레드가 대부분의 시간을 문자열 연결 처리에 소비하는 것으로 나타났다

여기 오늘의 핵심인데, 다른 누적 방식에 따라 효율성이 엄청나게 달라집니다. 기존 코드에서는 + 연산자를 사용했는데, 이 방식은 빈번하게 임시 객체를 생성하므로 효율이 매우 낮습니다. 그 비효율성을 알고는 있지만, 얼마나 비효율적인지 제대로 모르는 그런 종류입니다.

데모 검증

프로젝트 코드를 기반으로 비즈니스 로직을 분리하고, 문자열 연결 효율성 문제를 검증하기 위한 간단한 데모를 작성했습니다. windows 환경의 vs2022 컴파일러와 linux 환경의 gcc8.5 컴파일러로 Release 모드에서 컴파일 및 실행하여 효율성을 비교했습니다.

주요 사항 안내

프로젝트에서는 방법 네 가지를 사용했는데, 테스트 데이터를 받기 전 독자들은 어떤 방식이 가장 효율적이고 어떤 방식이 가장 비효율적인지 먼저 생각해 볼 수 있습니다. 결과를 보았을 때 저는 여전히 놀랐습니다.

방법 1 (+= 연결): 각 필드를 += 연산자를 사용하여 문자열에 직접 연결합니다 방법 2 ( std::ostringstream 연결): 스트림(std::ostringstream)을 사용하여 각 필드를 연결하는 방법으로, 특히 대량의 데이터를 연결할 때 더 효율적입니다 방법 3(미리 할당된 메모리의 += 연결): reserve를 사용하여 문자열에 필요한 충분한 메모리를 미리 할당하면 메모리 재할당 비용을 줄여 성능을 향상시킬 수 있습니다 방법 4(bodys = bodys + body + "\n"): 매번 연결할 때마다 새로운 임시 문자열 객체를 생성하므로, 특히 대규모 연결 시 성능 저하가 발생합니다. 이는 매번 연결이 새로운 메모리 할당 및 복사를 수반하기 때문입니다.

참고 결과, 프로젝트가 효율이 가장 낮은 방식을 선택한 것을 알 수 있습니다

더 나아가, 다양한 플랫폼 컴파일러의 최적화 효율을 분석해 보겠습니다. 마이크로소프트의 Visual Studio는 여전히 뛰어난 성능을 보여주며 문자열 최적화 효율이 매우 높지만, gcc 컴파일러는 이 부분에서 최적화 효율이 다소 떨어지는 편입니다.

코드 실행 환경이 다른 기계에서 이루어지므로 데이터 간의 직접적인 비교는 의미가 없습니다. 각각의 조인 방법 간의 차이를 비교하는 것이 좋습니다.

windows 平台下的 vs2022 编译器

----------------------------------------
Data Generation Time: 0.054 seconds.
----------------------------------------

----------------------------------------
Data Merging Performance:
----------------------------------------
+ Data merging (+=) took: 0.053 seconds.
+ ostringstream Data merging took: 0.054 seconds.
+ Pre-reserved Data merging took: 0.045 seconds.
+ Data merging (bodys = bodys + body + "\n") took: 16.108 seconds.

----------------------------------------
Data Merging Complete.
----------------------------------------

Program finished.
linux 平台下的 gcc8.5 编译器
----------------------------------------
Data Generation Time: 0.108 seconds.
----------------------------------------

----------------------------------------
Data Merging Performance:
----------------------------------------
+ Data merging (+=) took: 0.100 seconds.
+ ostringstream Data merging took: 0.083 seconds.
+ Pre-reserved Data merging took: 0.057 seconds.
+ Data merging (bodys = bodys + body + "\n") took: 29.298 seconds.

----------------------------------------
Data Merging Complete.
----------------------------------------

Program finished.

전체 코드

#include <iostream>
#include <string>
#include <vector>
#include <random>
#include <chrono>
#include <sstream>
#include <iomanip>

typedef std::vector<std::string> DataRow;
typedef std::vector<DataRow> DataGroup;

struct ResponsePackage
{
    std::string ErrorInfo;
    DataRow Head;
    std::string ClientId;
    std::string UUID;
    std::string MsgID;
    std::string SessionID;
    std::string ExtraInfo1;
    std::string ExtraInfo2;
    DataGroup DataBody;
};

// Generate specified length of random string
std::string generateRandomString(size_t length)
{
    const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    const size_t max_index = sizeof(charset) - 1;
    std::string random_string;
    random_string.reserve(length);

    std::random_device rd;
    std::mt19937 generator(rd());
    std::uniform_int_distribution<> distribution(0, max_index);

    for (size_t i = 0; i < length; ++i)
    {
        random_string += charset[distribution(generator)];
    }

    return random_string;
}

void create_large_string()
{
    // Example request package with 50 fields
    ResponsePackage requestPackage;

    requestPackage.Head = {
        "Field1", "Field2", "Field3", "Field4", "Field5",
        "Field6", "Field7", "Field8", "Field9", "Field10",
        "Field11", "Field12", "Field13", "Field14", "Field15",
        "Field16", "Field17", "Field18", "Field19", "Field20",
        "Field21", "Field22", "Field23", "Field24", "Field25",
        "Field26", "Field27", "Field28", "Field29", "Field30",
        "Field31", "Field32", "Field33", "Field34", "Field35",
        "Field36", "Field37", "Field38", "Field39", "Field40",
        "Field41", "Field42", "Field43", "Field44", "Field45",
        "Field46", "Field47", "Field48", "Field49", "Field50"
    };

    requestPackage.ClientId = "ClientID";
    requestPackage.UUID = "UUID";
    requestPackage.MsgID = "MsgID";
    requestPackage.SessionID = "SessionID";
    requestPackage.ExtraInfo1 = "ExtraInfo1";
    requestPackage.ExtraInfo2 = "ExtraInfo2";

    // Start timing for data generation
    auto start_gen = std::chrono::high_resolution_clock::now();

    // Generate 10,000 rows of data, each with 50 fields
    for (size_t i = 0; i < 10000; ++i)
    {
        DataRow dataRow(50, "This is a test string");
        requestPackage.DataBody.push_back(dataRow);
    }

    // End timing for data generation
    auto end_gen = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> duration_gen = end_gen - start_gen;

    // Display result generation time
    std::cout << "\n----------------------------------------\n";
    std::cout << "Data Generation Time: " << std::fixed << std::setprecision(3) << duration_gen.count() << " seconds.\n";
    std::cout << "----------------------------------------\n";

    // Data merging using different methods
    std::cout << "\n----------------------------------------\n";
    std::cout << "Data Merging Performance:\n";
    std::cout << "----------------------------------------\n";

    {
        // Method 1: Using '+=' string concatenation
        auto start_merge = std::chrono::high_resolution_clock::now();
        std::string bodys("");
        for (auto& vec : requestPackage.DataBody)
        {
            std::string body("This is a test string");
            for (auto& item : vec)
            {
                body += item + " ";
            }
            bodys += body + "\n";
        }
        auto end_merge = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double> duration_merge = end_merge - start_merge;
        std::cout << "+ Data merging (+=) took: " << std::fixed << std::setprecision(3) << duration_merge.count() << " seconds.\n";
    }

    {
        // Method 2: Using ostringstream
        auto start_merge = std::chrono::high_resolution_clock::now();
        std::ostringstream bodys;
        for (auto& vec : requestPackage.DataBody)
        {
            std::ostringstream body;
            body << "This is a test string";
            for (auto& item : vec)
            {
                body << item << " ";
            }
            bodys << body.str() << "\n";
        }
        auto end_merge = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double> duration_merge = end_merge - start_merge;
        std::cout << "+ ostringstream Data merging took: " << std::fixed << std::setprecision(3) << duration_merge.count() << " seconds.\n";
    }

    {
        // Method 3: Pre-allocated memory
        auto start_merge = std::chrono::high_resolution_clock::now();
        std::string bodys;
        bodys.reserve(1000 * 50 * 20); // Pre-allocate enough memory
        for (auto& vec : requestPackage.DataBody)
        {
            std::string body("This is a test string");
            body.reserve(50 * 20); // Pre-allocate memory for each row
            for (auto& item : vec)
            {
                body += item + " ";
            }
            bodys += body + "\n";
        }
        auto end_merge = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double> duration_merge = end_merge - start_merge;
        std::cout << "+ Pre-reserved Data merging took: " << std::fixed << std::setprecision(3) << duration_merge.count() << " seconds.\n";
    }

    {
        // Method 4: Using 'bodys = bodys + body + "\n"'
        auto start_merge = std::chrono::high_resolution_clock::now();
        std::string bodys("");
        for (auto& vec : requestPackage.DataBody)
        {
            std::string body("This is a test string");
            for (auto& item : vec)
            {
                body = body + item + " "; // Note the use of 'body = body + item'
            }
            bodys = bodys + body + "\n"; // Again, using 'bodys = bodys + body'
        }
        auto end_merge = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double> duration_merge = end_merge - start_merge;
        std::cout << "+ Data merging (bodys = bodys + body + \"\\n\") took: " << std::fixed << std::setprecision(3) << duration_merge.count() << " seconds.\n";
    }

    std::cout << "\n----------------------------------------\n";
    std::cout << "Data Merging Complete.\n";
    std::cout << "----------------------------------------\n";
}

int main()
{
    try
    {
        create_large_string();
    }
    catch (const std::exception& e)
    {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }

    std::cout << "\nProgram finished.\n";
    return 0;
}
Licensed under CC BY-NC-SA 4.0
마지막 수정: 2025년 05월 25일 14:10
금융 IT 프로그래머의 이것저것 만지작거리기와 일상의 중얼거림
Hugo로 만듦
JimmyStack 테마 사용 중