3

最近、あるインタビューでスレッドを使った文字列反転機能の実装を依頼されました。以下のソリューションの大部分を思いつきました。選ばれるかどうかは別の話です:-)。Windows 8 コンシューマー プレビューを実行している自宅の PC で、以下のソリューションを実行しようとしました。コンパイラは VC11 Beta です。

問題は、マルチスレッド コードが常にシーケンシャル コードと同じか、1 ミリ秒遅いかということです。私が与えた入力は、サイズが 32.4 MB のテキスト ファイルです。マルチスレッド コードを高速化する方法はありますか? それとも、与えられた入力が少なすぎて違いがないのでしょうか?

編集

インタビューでは方法void Reverse(char* str, int beg, int end, int rbegin, int rend);と方法だけを書きました。
void CustomReverse(char* str);他のすべてのコードは自宅で作成されます。

 template<typename Function>
    void TimeIt(Function&& fun, const char* caption)
    {
        clock_t start = clock();     
        fun();     
        clock_t ticks = clock()-start;     
        std::cout << std::setw(30) << caption          << ": "          << (double)ticks/CLOCKS_PER_SEC << "\n"; 
    }

    void Reverse(char* str)
    {


        assert(str != NULL);
        for ( int i = 0, j = strlen(str) - 1; i < j; ++i, --j)
        {
            if ( str[i] != str[j])
            {
                std::swap(str[i], str[j]);
            }
        }

    }

     void Reverse(char* str, int beg, int end, int rbegin, int rend)
        {
            for ( ; beg <= end && rbegin >= rend; ++beg, --rbegin)
            {
                if ( str[beg] != str[rbegin])
                {
                    char temp = str[beg];
                    str[beg] = str[rbegin];
                    str[rbegin] = temp;
                }
            }
        }

        void CustomReverse(char* str)
        {
            int len = strlen(str);
            const int MAX_THREADS = std::thread::hardware_concurrency();
            std::vector<std::thread> threads;

            threads.reserve(MAX_THREADS);

            const int CHUNK = len / MAX_THREADS > (4096) ? (4096) : len / MAX_THREADS;

            /*std::cout << "len:" << len << "\n";
            std::cout << "MAX_THREADS:" << MAX_THREADS << "\n";
            std::cout << "CHUNK:" << CHUNK << "\n";*/

        for ( int i = 0, j = len - 1; i < j; )
                {
                    if (i + CHUNK < j && j - CHUNK > i )
                    {
                        for ( int k = 0; k < MAX_THREADS && (i + CHUNK < j && j - CHUNK > i ); ++k)
                        {                                                
                             threads.push_back( std::thread([=, &str]() { Reverse(str, i,    
                                                    i + CHUNK, j, j - CHUNK); }));
                            i += CHUNK + 1;
                            j -= CHUNK + 1;
                        }


                        for ( auto& th : threads)
                        {
                            th.join();
                        }

                        threads.clear();
                    }
                    else
                    {          
                                        char temp = str[i];
                                        str[i] = str[j];
                                        str[j] = str[i];
                        i++;
                        j--;
                    }
                }
            }


        void Write(std::ostream&& os, const std::string& str)
        {
           os << str << "\n";
        }

        void CustomReverseDemo(int argc, char** argv)
        {
            std::ifstream inpfile;
            for ( int i = 0; i < argc; ++i)
                std::cout << argv[i] << "\n";

            inpfile.open(argv[1], std::ios::in);

            std::ostringstream oss;
            std::string line;

            if (! inpfile.is_open())
            {
                return;
            }
            while (std::getline(inpfile, line))
            {
                oss << line;
            }

            std::string seq(oss.str());
            std::string par(oss.str());

            std::cout << "Reversing now\n";

            TimeIt( [&] { CustomReverse(&par[0]); }, "Using parallel code\n");  
            TimeIt( [&] { Reverse(&seq[0]) ;}, "Using Sequential Code\n");
            TimeIt( [&] { Reverse(&seq[0]) ;}, "Using Sequential Code\n");
            TimeIt( [&] { CustomReverse(&par[0]); }, "Using parallel code\n");      



            Write(std::ofstream("sequential.txt"), seq);
            Write(std::ofstream("Parallel.txt"), par);
        }

        int main(int argc, char* argv[])
        {
            CustomReverseDemo(argc, argv);
        }
4

6 に答える 6

5

コードがわかりにくいと感じましたが、次の問題が見つかりました。

  • 4096 のブロック サイズは小さすぎて、スレッドの価値がありません。スレッドの開始は、実際の操作と同じくらいコストがかかる場合があります。
  • あなたはたくさんのフォークに参加しています(CHUNK * MAX_THREADS文字ごとに)。これにより、多くの不要な結合ポイント (シーケンシャル パーツ) とオーバーヘッドが発生します。

文字列を MAX_THREADS 個のチャンクに静的に分割し、MAX_THREADS 個のスレッドを開始します。より効率的な方法がありますが、少なくともこれにより、速度が向上します。

于 2012-05-13T17:30:58.950 に答える
1

300 MB の文字列サイズから始めて、マルチスレッド バージョン (TBB ベース、以下を参照) は、シリアル バージョンよりも平均 3 倍優れたパフォーマンスを発揮します。この 3 倍のスピードアップのために、12 個のリアル ハードウェア コアを使用していることを認めなければなりません :)。粒度を少し試してみました (blocked_range クラス オブジェクトの TBB で指定できます) が、これは大きな影響を与えませんでした。デフォルトの auto_partitioner は、データをほぼ最適に分割できるようです。私が使用したコード:

tbb::parallel_for(tbb::blocked_range<size_t>(0, (int)str.length()/2), [&] (const tbb::blocked_range<size_t>& r) {
    const size_t r_end = r.end();
    for(size_t i = r.begin(); i < r_end; ++i) {
        std::swap(*(std::begin(str) + i), *(std::end(str) - 1 - i));
    }
});
于 2012-05-16T13:14:33.717 に答える
1

新しいスレッド機能をすべて使用していますが、標準ライブラリの古い優れた部分をすべて使用しているわけではありませんstd::stringiterators

スレッド処理を自分で作成するのではなく、parallel_forコンストラクトのようなものを提供する並列アルゴリズム ライブラリを使用する必要があります。

タスクは次のように単純化できます。

  std::string str;

  // fill string

  auto worker = [&] (iter begin, iter end) {
      for(auto it = begin; it != end; ++it) {
        std::swap(*it, *(std::end(str) - std::distance(std::begin(str), it) - 1));
      }
  };

  parallel_for(std::begin(str), 
    std::begin(str) + std::distance(std::begin(str), std::end(str)) / 2, worker);

この並列アプローチを高速化するには、かなり大きなテキスト ファイルが必要であることに注意してください。34 MB では不十分な場合があります。

小さな文字列では、偽共有などの影響がパフォーマンスに悪影響を及ぼす可能性があります。

于 2012-05-13T17:31:05.017 に答える
1

同じ機能を持つプログラムを作成しようとしました: 「スレッドを使用して文字列を逆にする」という私の努力

Windows 7 で VC11 Beta と mingw(gcc 4.8) を搭載した 2 コア プロセッサでテストしました。

テスト結果:

VC11 ベータ版:

7 Mbファイル:

デバッグ

単純逆: 0.468

非同期リバース: 0.275

リリース

単純反転:0.006

非同期リバース: 0.014

98 Mbファイル:

デバッグ

シンプルリバース:5.982

非同期リバース: 3.091

リリース

単純反転: 0.063

非同期リバース: 0.079

782メガバイトのファイル

リリース

単純反転: 0.567

非同期リバース: 0.689

ミンウ:

782メガバイトのファイル

リリース

単純反転: 0.583

非同期リバース: 0.566

ご覧のとおり、マルチスレッド コードはデバッグ ビルドでのみ有効です。しかし、リリースされたコンパイラは最適化を行い、シングルスレッド コードの場合でもすべてのコアを使用します。

だからあなたのコンパイラを信頼してください=)

于 2012-05-16T13:27:59.150 に答える
1
  • チャンクサイズを 4096 に制限しても意味がありません。
  • 一度初期化し、最後に同期することは、常に並列操作のパターンでなければなりません (map/reduce を考えてください)。

小さいこと:

  • 文字が同一であるかどうかを確認することは、あらゆる種類のパイプラインの最適化にとってすべて悪いことです。swap() を実行するだけです。
  • 並列バージョンと順次バージョンでは、スワップに異なるコードを使用します。なぜ?
于 2012-05-13T17:38:32.233 に答える
0

テスト済みコード

#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <string.h>
#include <stdio.h>
#include <memory.h>
#include <stdlib.h>

void strrev(char *p, char *q, int num)
{
    for(int i=0;i < num ; ++i,--q, ++p)
        *q = *p;
}

int main(int argc, char **argv)
{
    char *str;
    if(argc>1)
    {
        str = argv[1];
        printf("String to be reversed %s\n", str);
    }
    else
    {
        return 0;
    }

    int length = strlen(str);
    int N = 5;
    char *rev_str = (char *)malloc(length+1);
    rev_str[length] = '\0';

    if (N>length)
    {
        N = length;
    }

    std::vector<std::thread> threads;

    int begin=0, end=length-1, k = length/N;
    for(int i=1; i <= N; ++i)
    {
        threads.emplace_back(strrev, &str[begin], &rev_str[end], k);
        //strrev(&str[begin], &rev_str[end], k);
        begin += k;
        end -= k;
    }

    while (true)
    {
        if (end < 0 && begin > length-1)
        {
            break;
        }
        rev_str[end] = str[begin];
        --end; ++begin;
    }

    for (auto& i: threads)
    {
        i.join();
    }

    printf("String after reversal %s\n", rev_str);

    return 0;
}
于 2014-05-01T10:28:05.173 に答える