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

当你使用所有新的线程特性时,你并没有使用标准库的所有旧的好部分,比如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
  • 将块大小限制为 4096 没有任何意义。
  • 初始化一次,然后在最后同步应该始终是并行操作的模式(想想 map/reduce)

较小的东西:

  • 检查字符是否相同对于任何类型的管道优化都是不利的。只需执行交换()。
  • 在并行和顺序版本中,您使用不同的代码进行交换。为什么?
于 2012-05-13T17:38:32.233 回答
1

从 300 MB 字符串大小开始,我看到多线程版本(基于 TBB,见下文)的性能平均比串行版本好 3 倍。不得不承认,对于这 3 倍的加速,它使用了 12 个真实硬件核心 :)。我对粒度进行了一些试验(您可以在 TBB 中为 blocksed_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));
    }
});
于 2012-05-16T13:14:33.717 回答
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 MB文件

释放

简单反向:0.567

异步反向:0.689

明武:

782 MB文件

释放

简单反向:0.583

异步反向:0.566

如您所见,多线程代码仅在调试构建中获胜。但是在发行版中,即使是单线程代码,编译器也会进行优化并使用所有内核。

所以相信你的编译器=)

于 2012-05-16T13:27:59.150 回答
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 回答