Linuxバックエンドサービスの大量文字列データの処理 - 効率が悪い

C++開発の歴史的なプロジェクトにおいて、カスタムプロトコルを使用して通信を行っており、そのプロトコルは2次元配列のパターンを採用していました。大量データを処理する際に、プロトコル内部では配列を遍历し、シリアライズ操作を実行してログを生成しており、このため効率が低く、システムが高負荷時に顕著なフレーム落ち(カドゥ)を引き起こしました。事業部門からは、システムのフレーム落ちに関するフィードバックがありました。

問題の特定

問題のトラブルシューティングにおいて、まずシステムに対してパフォーマンス分析を実施し、大量データを処理する際にCPU使用率が著しく増加し、システムの応答時間が長くなっていることを発見しました。ログを分析した結果、多数のシリアライズ操作が見られ、これらの操作は2次元配列を処理する際の効率が低いことが原因でシステム性能が低下していました。 pstackツールを使用してサービスのスレッド情報を取得し、ログスレッドが文字列の連結に大部分の時間を使用していることを特定しました。

今日は重点的に取り組むべき点です。異なる累積方式では、その効率の違いは非常に大きいです。過去のコードでは ‘+’ 演算子を使用しており、この方法は頻繁に一時オブジェクトを作成するため、非常に非効率的でした。それは、その非効率さがどの程度であるかを知らない状況にあるようなものです。

デモ検証

プロジェクトコードに基づいて、ビジネスロジックを抽出し、文字列連結の効率に関する問題を検証するためのシンプルなデモを作成しました。Windows環境ではVisual Studio 2022コンパイラ、Linux環境ではgcc8.5コンパイラを使用し、Releaseモードでビルドして実行することで、効率を比較します。

キーポイントの説明

本プロジェクトで使用されたのは方法四であり、テストデータを入手する前に、どの方法が最も効率が良いか、最も効率が悪いかを読者が最初に考えてみるべきです。結果を見たときは、自分自身にとても驚きました。

  1. 方法 1 (+= による連結):各フィールドを += を使って文字列に直接連結します。
  2. 方法 2 (std::ostringstream による連結):ストリーム(std::ostringstream)を使用して各フィールドを連結する方法で、特に大量のデータを連結する場合に効率的です。
  3. 方法 3 (事前にメモリを割り当てる += による連結)reserve を使用して文字列に必要な十分なメモリを事前に割り当て、メモリ再割り当てのオーバーヘッドを削減することでパフォーマンスを向上させます。
  4. 方法 4 (bodys = bodys + body + "\n"): 各連結で新しい一時的な文字列オブジェクトを作成するため、パフォーマンスが低下します。特に大規模な連結の場合、各連結において新しいメモリ割り当てとコピーが発生するためです。

参照結果から、プロジェクトはちょうど最も効率の悪い方法を選択していました。

さらに詳しく分析すると、異なるプラットフォームコンパイラによる最適化効率を分析できます。Microsoft の visual studio は従来通り優れており、文字列の最適化効率は非常に高いですが、gcc コンパイラの最適化効率は少し劣ります。

異なるマシンでコードを実行した場合、2つのデータセット間で直接比較する意味はありません。異なる連結方法間の差を比較できます。

主要ポイント

Windowsプラットフォーム下でのVisual Studio 2022コンパイラ

----------------------------------------
データ生成時間: 0.054秒。
----------------------------------------

----------------------------------------
データマージパフォーマンス:
----------------------------------------
+ データマージ (+=) にかかった時間: 0.053秒。
+ ostringstream データマージにかかった時間: 0.054秒。
+ 事前予約済みデータマージにかかった時間: 0.045秒。
+ データマージ (bodys = bodys + body + "\n") にかかった時間: 16.108秒。

----------------------------------------
データマージ完了。
----------------------------------------

プログラム終了。
Linuxプラットフォーム下でのgcc8.5コンパイラ
----------------------------------------
データ生成時間: 0.108秒。
----------------------------------------

----------------------------------------
データマージパフォーマンス:
----------------------------------------
+ データマージ (+=) にかかった時間: 0.100秒。
+ ostringstream データマージにかかった時間: 0.083秒。
+ 事前予約済みデータマージにかかった時間: 0.057秒。
+ データマージ (bodys = bodys + body + "\n") にかかった時間: 29.298秒。

----------------------------------------
データマージ完了。
----------------------------------------

プログラム終了。

完整コード

#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

## 完全なコード
```json
{
        // 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;
}
金融ITプログラマーのいじくり回しと日常のつぶやき
Hugo で構築されています。
テーマ StackJimmy によって設計されています。