使用线程反转字符串

时间:2012-05-13 16:22:44

标签: c++ multithreading algorithm c++11

最近,我在接受采访时被要求使用线程实现字符串反转功能。我想出了下面解决方案的大部分内容。选择与否是一个不同的故事:-)。我试图在运行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);
        }

6 个答案:

答案 0 :(得分:5)

我发现代码难以理解,但我发现了以下问题:

  • 您的块大小4096太小,不值得一个线程。启动一个线程可能与实际操作一样昂贵。
  • 您正在进行大量加密(对于每个CHUNK * MAX_THREADS个字符)。这引入了许多不需要的连接点(顺序部分)和开销。

将字符串静态分区为MAX_THREADS块并启动MAX_THREADS个线程。有更有效的方法可以做到这一点,但至少这会给你一些加速。

答案 1 :(得分: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可能还不够。

在小字符串上,false sharing之类的效果可能会对您的表现产生负面影响。

答案 2 :(得分:1)

  • 将chunksize限制为4096没有任何意义。
  • 初始化一次然后在最后进行同步应该始终是并行操作的模式(想想map / reduce)

小事:

  • 检查字符是否相同对于任何类型的管道优化都是不利的。只需执行swap()。
  • 在并行和顺序版本中,您使用不同的交换代码。为什么呢?

答案 3 :(得分:1)

从300 MB字符串大小开始,我发现多线程版本(基于TBB,见下文)的性能平均比串行版本好3倍。不得不承认,对于这个3倍的加速,它使用12个真正的核心:)。我尝试了一些粒度(你可以在TBB中为blocked_range类对象指定那些),但是这没有产生任何重大影响,默认的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));
    }
});

答案 4 :(得分:1)

我尝试用相同的功能编写程序: My effort of "Reversing a string using threads"

我已经在Windows 7上测试了带有VC11 Beta和mingw(gcc 4.8)的2核心处理器

测试结果:

VC11 Beta:

7 Mb 档案:

调试

简单反转:0.468

异步反转:0.275

推出

简单反转:0.006

异步反转:0.014

98 Mb 档案:

调试

简单反转:5.982

异步反转:3.091

推出

简单反转:0.063

异步反转:0.079

782 Mb 档案

推出

简单反转:0.567

异步反转:0.689

<强> MINGW:

782 Mb 档案

推出

简单反转:0.583

异步反转:0.566

正如您所看到的,多线程代码仅在调试版本中获胜。但是在发布编译器中,即使在单线程代码的情况下也会进行优化并使用所有内核。

所以相信你的编译器=)

答案 5 :(得分: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;
}