【问题标题】:Why is Python faster than C++ in this case?在这种情况下,为什么 Python 比 C++ 快?
【发布时间】:2014-09-13 18:36:41
【问题描述】:

下面给出了一个 Python 和 C++ 程序,它执行以下任务:从 stdin 读取空格分隔的单词,将按字符串长度排序的唯一单词以及每个唯一单词的计数打印到 stdout。输出的一行格式为:length、count、word。

例如,使用这个输入文件(488kB 的词库) http://pastebin.com/raw.php?i=NeUBQ22T

带有格式的输出是这样的:

1 57 "
1 1 n
1 1 )
1 3 *
1 18 ,
1 7 -
1 1 R
1 13 .
1 2 1
1 1 S
1 5 2
1 1 3
1 2 4
1 2 &
1 91 %
1 1 5
1 1 6
1 1 7
1 1 8
1 2 9
1 16 ;
1 2 =
1 5 A
1 1 C
1 5 e
1 3 E
1 1 G
1 11 I
1 1 L
1 4 N
1 681 a
1 2 y
1 1 P
2 1 67
2 1 y;
2 1 P-
2 85 no
2 9 ne
2 779 of
2 1 n;
...

这是C++程序

#include <vector>
#include <string>
#include <iostream>
#include <set>
#include <map>

bool compare_strlen (const std::string &lhs, const std::string &rhs) {
  return (lhs.length() < rhs.length());
}

int main (int argc, char *argv[]) {
  std::string str;
  std::vector<std::string> words;

  /* Extract words from the input file, splitting on whitespace */
  while (std::cin >> str) {
    words.push_back(str);
  }

  /* Extract unique words and count the number of occurances of each word */
  std::set<std::string> unique_words;
  std::map<std::string,int> word_count; 
  for (std::vector<std::string>::iterator it = words.begin();
       it != words.end(); ++it) {
    unique_words.insert(*it);
    word_count[*it]++;
  }

  words.clear();
  std::copy(unique_words.begin(), unique_words.end(),
            std::back_inserter(words));

  // Sort by word length 
  std::sort(words.begin(), words.end(), compare_strlen);

  // Print words with length and number of occurances
  for (std::vector<std::string>::iterator it = words.begin();
       it != words.end(); ++it) {
    std::cout << it->length() << " " << word_count[*it]  << " " <<
              *it << std::endl;
  }

  return 0;
}

这是 Python 程序:

import fileinput
from collections import defaultdict

words = set()
count = {}
for line in fileinput.input():
  line_words = line.split()
  for word in line_words:
    if word not in words:
      words.add(word)
      count[word] = 1
    else:
      count[word] += 1 

words = list(words)
words.sort(key=len)

for word in words:
  print len(word), count[word], word

对于 C++ 程序,使用的编译器是带有 -O3 标志的 g++ 4.9.0。

使用的 Python 版本是 2.7.3

C++ 程序所用时间:

time ./main > measure-and-count.txt < ~/Documents/thesaurus/thesaurus.txt

real    0m0.687s
user    0m0.559s
sys     0m0.123s

Python 程序所用时间:

time python main.py > measure-and-count.txt < ~/Documents/thesaurus/thesaurus.txt

real    0m0.369s
user    0m0.308s
sys     0m0.029s

Python 程序比 C++ 程序快得多,并且在更大的输入大小下相对更快。这里发生了什么?我对 C++ STL 的使用不正确吗?

编辑: 根据评论和答案的建议,我将 C++ 程序更改为使用 std::unordered_setstd::unordered_map

以下几行已更改

#include <unordered_set>
#include <unordered_map>

...

std::unordered_set<std::string> unique_words;
std::unordered_map<std::string,int> word_count;

编译命令:

g++-4.9 -std=c++11 -O3 -o main main.cpp

这只是略微提高了性能:

time ./main > measure-and-count.txt < ~/Documents/thesaurus/thesaurus.txt

real    0m0.604s
user    0m0.479s
sys     0m0.122s

Edit2:更快的 C++ 程序

这是 NetVipeC 的解决方案、Dieter Lücking 的解决方案和this question 的最佳答案的组合。真正的性能杀手是cin 默认使用无缓冲读取。已解决std::cin.sync_with_stdio(false);。此解决方案还使用单个容器,利用 C++ 中的有序 map

#include <vector>
#include <string>
#include <iostream>
#include <set>
#include <map>

struct comparer_strlen {
    bool operator()(const std::string& lhs, const std::string& rhs) const {
        if (lhs.length() == rhs.length())
            return lhs < rhs;
        return lhs.length() < rhs.length();
    }
};

int main(int argc, char* argv[]) {
    std::cin.sync_with_stdio(false);

    std::string str;

    typedef std::map<std::string, int, comparer_strlen> word_count_t;

    /* Extract words from the input file, splitting on whitespace */
    /* Extract unique words and count the number of occurances of each word */
    word_count_t word_count;
    while (std::cin >> str) {
        word_count[str]++;
    }

    // Print words with length and number of occurances
    for (word_count_t::iterator it = word_count.begin();
         it != word_count.end(); ++it) {
        std::cout << it->first.length() << " " << it->second << " "
                  << it->first << '\n';
    }

    return 0;
}

运行时

time ./main3 > measure-and-count.txt < ~/Documents/thesaurus/thesaurus.txt

real    0m0.106s
user    0m0.091s
sys     0m0.012s

Edit3: Daniel 提供了一个简洁明了的 Python 程序版本,它的运行时间与上述版本大致相同:

import fileinput
from collections import Counter

count = Counter(w for line in fileinput.input() for w in line.split())

for word in sorted(count, key=len):
  print len(word), count[word], word

运行时:

time python main2.py > measure-and-count.txt.py < ~/Documents/thesaurus/thesaurus.txt

real    0m0.342s
user    0m0.312s
sys     0m0.027s

【问题讨论】:

  • std::map vs std::unordered_map ?
  • 我没有足够的信心来欺骗这个,但这里可能是重复的:stackoverflow.com/questions/9371238/…
  • 顺便说一句。使用 Python 是因为它的简短而不是速度:count = Counter(l for line in fileinput.input() for l in line.split()) for word in sorted(count, key=len): print len(word), count[word], word
  • 尝试降低优化设置。拥有-O3 在某些情况下会降低 性能。尝试不同的优化设置。也可以尝试标记比较函数inline
  • -O2 在我的机器上稍慢~(-10ms)

标签: python c++ performance io


【解决方案1】:

用这个测试一下,肯定比原来的C++快。

变化是:

  • 删除了向量words来保存单词(word_count中已经保存了)。
  • 消除了集合unique_words(word_count 中只有唯一的词)。
  • 将第二个副本删除为单词,不需要。
  • 去掉了单词的排序(地图中的顺序更新了,现在地图中的单词是按长度排序的,相同长度的单词是按字典顺序排列的。

    #include <vector>
    #include <string>
    #include <iostream>
    #include <set>
    #include <map>
    
    struct comparer_strlen_functor {
        operator()(const std::string& lhs, const std::string& rhs) const {
            if (lhs.length() == rhs.length())
                return lhs < rhs;
            return lhs.length() < rhs.length();
        }
    };
    
    int main(int argc, char* argv[]) {
        std::cin.sync_with_stdio(false);
    
        std::string str;
    
        typedef std::map<std::string, int, comparer_strlen_functor> word_count_t;
    
        /* Extract words from the input file, splitting on whitespace */
        /* Extract unique words and count the number of occurances of each word */
        word_count_t word_count;
        while (std::cin >> str) {
            word_count[str]++;
        }
    
        // Print words with length and number of occurances
        for (word_count_t::iterator it = word_count.begin(); it != word_count.end();
             ++it) {
            std::cout << it->first.length() << " " << it->second << " " << it->first
                      << "\n";
        }
    
        return 0;
    }
    

新版本的阅读循环,逐行阅读并拆分。需要#include &lt;boost/algorithm/string/split.hpp&gt;

while (std::getline(std::cin, str)) {
    for (string_split_iterator It = boost::make_split_iterator(
             str, boost::first_finder(" ", boost::is_iequal()));
         It != string_split_iterator(); ++It) {
        if (It->end() - It->begin() != 0)
            word_count[boost::copy_range<std::string>(*It)]++;
    }
}

在 Core i5、8GB RAM、GCC 4.9.0、32 位中进行测试,运行时间为 238 毫秒。按照建议使用std::cin.sync_with_stdio(false);\n 更新了代码。

【讨论】:

  • 在我的机器上,你的版本在0.540s 上只稍微快一点。
  • g++-4.9 -std=c++11 -O3 -o main2 main2.cpp
  • 两个程序都执行了多次?
  • 是的,衡量性能的正确方法是多次运行的平均值。在这种情况下,我看到性能在 +-5 毫秒内运行测试 10 次左右,然后粘贴有代表性的结果。
  • 我喜欢这个答案,因为它利用了 STL 中默认 map 的排序属性的优势。然而,最终答案中的大部分加速来自使用std::cin.sync_with_stdio(false);'\n' 而不是std::endl
【解决方案2】:

进行三处更改,省略额外的向量(python 中没有),为单词向量保留内存并避免输出中的 endl (!):

#include <algorithm>
#include <vector>
#include <string>
#include <iostream>
#include <set>
#include <map>

bool compare_strlen (const std::string &lhs, const std::string &rhs) {
  return (lhs.length() < rhs.length());
}

int main (int argc, char *argv[]) {
    /* Extract words from the input file, splitting on whitespace */
    /* Also count the number of occurances of each word */
    std::map<std::string, int> word_count;
    {
        std::string str;
        while (std::cin >> str) {
            ++word_count[str];
        }
    }

    std::vector<std::string> words;
    words.reserve(word_count.size());
    for(std::map<std::string, int>::const_iterator w = word_count.begin();
        w != word_count.end();
        ++w)
    {
        words.push_back(w->first);
    }

    // Sort by word length
    std::sort(words.begin(), words.end(), compare_strlen);

    // Print words with length and number of occurances
    for (std::vector<std::string>::iterator it = words.begin();
       it != words.end();
       ++it)
    {
        std::cout << it->length() << " " << word_count[*it]  << " " <<
                  *it << '\n';
    }
    return 0;
}

给予:

原文:

real    0m0.230s
user    0m0.224s
sys 0m0.004s

改进:

real    0m0.140s
user    0m0.132s
sys 0m0.004s

通过添加std::cin.sync_with_stdio(false); 得到更多改进请参阅 OregonTrail 的问题):

real    0m0.107s
user    0m0.100s
sys 0m0.004s

NetVipeC 的解决方案是 std::cin.sync_with_stdio(false); 和 std::endl 替换为 '\n':

real    0m0.077s
user    0m0.072s
sys 0m0.004s

Python:

real    0m0.146s
user    0m0.136s
sys 0m0.008s

【讨论】:

  • 您的版本在我的机器上仍然比 Python 慢大约 30%,但它是迄今为止最快的解决方案。
  • 哇,从 NetVipeC 的解决方案中删除 std::endl 只会比我机器上的 Python 程序慢 8%。
  • @OregonTrail - 我的速度相同(g++ -O3 main.cc -o 测试)
【解决方案3】:
  std::vector<std::string> words;

  /* Extract words from the input file, splitting on whitespace */
  while (std::cin >> str) {
    words.push_back(str);
  }

这需要随着向量的增长不断重复分配/复制/释放操作。要么预先分配向量,要么使用列表之类的东西。

【讨论】:

  • Python 程序没有进行任何预分配。此外,预分配需要计算文件中的单词。
  • @OregonTrail 如果你明白我在说什么,你的回答是不合适的。 (而且它不需要计算它们。只需预先分配,比如说,一百万个条目就可以了。另外,你怎么知道 Python 程序是否预先分配?)
  • 你试过了吗?我看不出有什么显着差异。我猜它是由IO主导的。
  • Python 程序动态增长set(),Python 必须realloc() 这个数据结构(至少是哈希表,除非它对于set() 具有异常大的默认大小)。此外,为了从不C++程序中复制向量本身,它必须提前计算输入文件中的单词,然后倒回文件。
  • @DavidSchwartz 我认为你很接近。我相信 Python 必须从输入文件中使用某种形式的缓冲读取。 cin 不会在内存中缓冲文件,因此每次读取都需要 IO。相反,应该读取整个文件(或批量)并在内存中执行解析。
【解决方案4】:

您的 C++ 代码存在一些问题。

首先,您使用的是可变字符串。这意味着您正在大量复制它们。 (Python 字符串是不可变的)。对此进行测试,我发现效果实际上可以使C++代码变慢,所以让我们放弃这个。

其次,unordered_ 容器可能是个好主意。对此进行测试,我通过将它们换入/换出(使用boost::hash 散列算法)获得了 1/3 的加速。

第三,您对std::endl 的使用会在每一行上刷新std::cout。这似乎很愚蠢。

第四,std::cin.sync_with_stdio(false); 以减少 std::cin 的开销,或者不要使用它们。

第五,直接从io构造setmap,不要无谓地往返std::vector

这是一个test program(硬编码数据大约是大小的 1/4),带有不可变字符串 (std::shared_ptr&lt;const std::string&gt;) 和带有手动哈希设置的 unordered_ 容器,以及一些 C++11 功能代码有点短。

删除大的R"( 字符串文字,并将stringstream 替换为std::cin

为了提高性能,不要使用重量级的流对象。他们做了很多非常非常偏执的工作。

【讨论】:

  • 最后一点,使用 std::cin.sync_with_stdio(false); 给了我 4 倍的加速。最新版本在原帖底部。
  • @OregonTrail 已更新。比原来快大约 5 倍(从 0.05 下降到 0.01)。包含可在基于智能指针的字符串和按值字符串以及无序容器与有序容器之间轻松交换的代码。
【解决方案5】:

这是另一个 C++ 版本,我认为它更接近于逐行匹配 python。它试图保留与 python 版本中相同类型的容器和操作,并进行明显的 C++ 特定调整。请注意,我从其他答案中提升了 sync_with_stdio 优化。

#include <iostream>
#include <unordered_set>
#include <unordered_map>
#include <list>

#include <sstream>
#include <iterator>


bool compare_strlen(const std::string &lhs, const std::string &rhs)
{
    return lhs.length() < rhs.length();
}

int main (int argc, char *argv[]) {
    std::unordered_set<std::string> words;
    std::unordered_map<std::string, std::size_t> count;

    // Make std::cin use its own buffer to improve I/O performance.
    std::cin.sync_with_stdio(false);

    // Extract words from the input file line-by-line, splitting on
    // whitespace
    char line[128] = {};  // Yes, std::vector or std::array would work, too.
    while (std::cin.getline(line, sizeof(line) / sizeof(line[0]))) {
        // Tokenize
        std::istringstream line_stream(line);
        std::istream_iterator<std::string> const end;
        for(std::istream_iterator<std::string> i(line_stream);
            i != end;
            ++i) {
            words.insert(*i);
            count[*i]++;
        }
    }

    std::list<std::string> words_list(words.begin(), words.end());
    words_list.sort(compare_strlen);

    // Print words with length and number of occurences
    for (auto const & word : words_list)
        std::cout << word.length()
                  << ' ' << count[word]
                  << ' ' << word
                  << '\n';

    return 0;
}

结果与您的原始 python 代码和@NetVipeC 的 C++ 相当。

C++

real    0m0.979s
user    0m0.080s
sys     0m0.016s

Python

real    0m0.993s
user    0m0.112s
sys     0m0.060s

我有点惊讶这个版本的 C++ 的性能与您问题的其他简化 C++ 答案相当,因为我确信像基于 stringstream 的标记化这样的事情会成为瓶颈。

【讨论】:

    【解决方案6】:

    std::setstd::map 都针对查找而不是插入进行了优化。每次更改内容时都必须对它们进行排序/树平衡。您可以尝试使用基于散列的std::unordered_setstd::unordered_map,并且对于您的用例会更快。

    【讨论】:

    • 添加了使用 unordered 容器的结果,仅略微提高了性能。
    猜你喜欢
    • 2022-01-10
    • 2012-08-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多