C++程序度量驱动调优实例:看狄更斯的双城记,寻找性能瓶颈

作为一个专业的程序员,你写了一段程序,完成了一个功能,似乎达到了用户的要求,其实你心里也没底。
做一个简单的测试, 跑了一个主要流程,基本的功能应该是满足需求的,但是性能呢?

我们需要对程序的性能进行度量,确保性能是满足用户需求的。如果性能不理想,必需要找到瓶颈在哪里。

最近在读狄更斯的小说《双城记》英文原版,既是学习英语,也是领略大师的行文。同时,我想知道大师喜欢用什么词,所以写了一个小程序来统计这部小说的词汇。让我们度量一下这个小程序的性能,看看瓶颈在哪里, 有什么好的方法。

程序示例

需求

设计

用C++ 来写一段小程序,取名为 WordBank , 基本功能就是

  1. 获取总的单词数量: getTotalWordCount();
  2. 获取不重复的单词总数:getUniqueWordCount);
  3. 打印若干个使用频率最高的单词:printTop(int n)
  4. 获取某个单词的使用频度排序号: getWordRank(const std::string& name);
  5. 获取某个单词的使用数量:getWordCount(const std::string& name);

我想看看

  • 大师在这本书中使用了多少词汇
  • 大师用的最多的20个单词
  • "think" 这个单词用了多少次
  • "think" 排在最常使用率排名的多少位

于是,我就写了一个C++ 类 WordBank, 使用 std::map<std::string, int> m_mapWords; 来保存单词的个数,使用 std::vector<std::pair<std::string, int>> m_vecWords; 来保存排过序的单词使用率排名。

#ifndef __WORD_BANK_H__
#define __WORD_BANK_H__

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

class WordBank
{
public:
    WordBank(const std::string& words_file);

    int getWordRank(const std::string& name) const;

    int getWordCount(const std::string& name) const;

    void sortWords();

    void printTop(int n) const;

    int getTotalWordCount() const;

    int getUniqueWordCount() const;

private:

    bool hasWord(const std::string& word) const;

    void increaseWordCount(const std::string& word);

    void addWord(const std::string& word);

    std::map<std::string, int> m_mapWords;

    std::vector<std::pair<std::string, int>> m_vecWords; 

    uint32_t m_wordCount;

};

std::string convertString(const std::string& word);

#endif

实现

#include "WordBank.h"
#include <iostream>
#include <fstream>
#include <algorithm>

using namespace std;

bool comparePair(pair<string, int>& a, 
         pair<string, int>& b) 
{ 
    return a.second > b.second; 
} 

WordBank::WordBank(const string & words_file): m_wordCount(0) {
    ifstream is(words_file.data());
    if (!is) {
        throw invalid_argument("unable open file");
    }
    string word;
    while (is >> word) {
        m_wordCount ++;
        if (!hasWord(word)) {
            addWord(word);
        }else {
            increaseWordCount(word);
        }
    }
}

int WordBank::getWordRank(const std::string& word) const {
    
    int count = getWordCount(word);
    if (count == 0) {
        return 0;
    }
    
    int rank = 1;
    for (const auto&[key, value] : this->m_mapWords) {
        if (value > count) {
            rank++;
        }
    }
    return rank;
}

int WordBank::getWordCount(const std::string& word) const {
    string str = convertString(word.data());
    auto it = m_mapWords.find(str);
    
    if (it != m_mapWords.end()) {
        return it->second;
    }
    return 0;
}


int WordBank::getTotalWordCount() const {
    return m_wordCount;
}

int WordBank::getUniqueWordCount() const {
    return m_mapWords.size();
}

bool WordBank::hasWord(const string & word) const {
    string str = convertString(word.data());
    auto it = m_mapWords.find(str);
    
    if (it != m_mapWords.end()) {
        return true;
    }
    return false;
}
void WordBank::increaseWordCount(const string & word) {
    string str = convertString(word.data());
    auto it = m_mapWords.find(str);

    if (it != m_mapWords.end()) {
        it->second++;
    }
}
void WordBank::addWord(const string & word) {
    
    string str = convertString(word);
    if (str.empty()) {
        return;
    }
    //cout << "insert " << word  << " -> " << str << endl;
    m_mapWords.insert(make_pair(str, 1));
}

void WordBank::sortWords() {
   
    for (auto& pair : m_mapWords ) { 
        m_vecWords.push_back(pair); 
    } 
  
    // Sort using comparator function 
    sort(m_vecWords.begin(), m_vecWords.end(), comparePair); 
  

} 

void WordBank::printTop(int n) const {
    for (auto pair : m_vecWords) {
        cout << pair.first << ": " << pair.second << endl;
        if (--n <= 0) {
            break;
        }
    }

}

string convertString(const string & word) {
    string str("");
    for (size_t i = 0; i < word.length(); ++i) {
        uint8_t ch = word[i];
        if(ch > 0 && ch < 255 && isalpha(ch)) {
            str.push_back(tolower(ch));
        }
    }
    return str;
}

测试

#include "WordBank.h"
#include <cstdlib>
#include <iostream>
#include <cmath>
#include <boost/timer/timer.hpp>


using namespace std;
using namespace boost::timer;

int main(int argc, char *argv[])
{
    string word_search = "rtp";
    string word_file = "rfc3550.txt";
    int topN = 10;
    if (argc > 1) {
        word_search = argv[1];
        cout << "Make statistics for word count and rank: " <<convertString(word_search) << endl;
    } else {
        cout << "usage: " << argv[0] << "<search_word> <input_file> <topN>" << endl;
        cout << "example: " << argv[0] << " " << word_search << " " << word_file << " " << topN <<endl;
    }
    if (argc > 2) {
        word_file = argv[2];
        cout << "Read file: " << word_file << endl;
    }
    if (argc > 3) {
        topN = atoi(argv[3]);
    }
    
    try {
        boost::timer::auto_cpu_timer timer;
        
        WordBank wordbank(word_file);
        wordbank.sortWords();
        std::cout << "wordbank total word count: " << wordbank.getTotalWordCount() << endl;
        std::cout << "wordbank unique word count: " << wordbank.getUniqueWordCount() << endl;
        cout << word_search << "'s count=" << wordbank.getWordCount(word_search) << endl;
        cout << word_search  << "'s rank=" << wordbank.getWordRank(word_search) << endl;
        cout << "--- top "  << topN << " ---" << endl;
        wordbank.printTop(topN);

    } catch(const invalid_argument& e) {
        cerr << "Caught exeption: " <<e.what() <<endl;

    }
    return 0;

}

执行结果如下, 双城记总词数为139021 ,共使用了 10984 个单词,最常使用的词是 "the", 而 “think” 这个词用了 120 次,排名 150 位。

./bin/wordbankdemo think A-Tale-of-Two-Cities.txt 20
Make statistics for word count and rank: think
Read file: A-Tale-of-Two-Cities.txt
wordbank total word count: 139021
wordbank unique word count: 10984
think's count=120
think's rank=150
--- top 20 ---
the: 8202
and: 4998
of: 4137
to: 3545
a: 2981
in: 2642
it: 2016
his: 2005
i: 1917
that: 1904
he: 1833
was: 1765
you: 1460
with: 1354
had: 1297
as: 1148
at: 1045
her: 1038
for: 973
him: 965
 0.079933s wall, 0.060000s user + 0.000000s system = 0.060000s CPU (75.1%)

度量性能

使用 boost 库的 auto_cpu_timer 来计时

剖析性能

使用著名的 valgrind 软件所附带的 callgrind 的剖析性能瓶颈在哪里

sudo apt install valgrind
valgrind --tool=callgrind ./bin/wordbankdemo
sudo apt-get install python3 graphviz

# 在 Debian/Ubuntu 安装 python3 和 graphviz
apt-get install python3 graphviz

# 在 RedHat/Fedora  安装 python3 和 graphviz
yum install python3 graphviz

# 再安装 gprof2dot
pip install gprof2dot

# 生成有向图
gprof2dot -f callgrind -n10 -s callgrind.out.816 > valgrind.dot
dot -Tpng valgrind.dot -o valgrind.png

从上图可以看出,其大部分时间花在 WordBank 的构造方法,其中 hasWord, increaseWordCount 这两个方法花了大部分时间, 其中 convertString 和map 的红黑树的 find 方法又占据了大部分时间, 仔细看看这两个方法的调用,其实我们能够做出优化

WordBank::WordBank(const string & words_file): m_wordCount(0) {
    ifstream is(words_file.data());
    if (!is) {
        throw invalid_argument("unable open file");
    }
    string word;
    while (is >> word) {
        m_wordCount ++;
        if (!hasWord(word)) {
            addWord(word);
        }else {
            increaseWordCount(word);
        }
    }
}
  1. word 可以事先转化 convertString 方法不需要重复调用
  2. hasWordincreaseWordCount 不需要重复查找, 可以把迭代器作为额外参数传入, 或者把 map 的 value 的引用传出来

试着改一下,再重复上述的度量过程, 你能清晰地看出来改进的效果。

代码链接参见

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值