替代C ++的标准函数以获得速度优化

时间:2012-05-01 15:08:57

标签: c++ performance optimization

只是为了澄清我也认为标题有点傻。我们都知道语言的大多数内置函数都写得很好而且速度很快(有些甚至是汇编编写的)。虽然可能对我的情况仍有一些建议。我有一个小项目,展示了搜索引擎的工作。在索引阶段,我有一个过滤方法来从关键字中过滤掉不必要的东西。就在这里:

bool Indexer::filter(string &keyword)
{
    // Remove all characters defined in isGarbage method
    keyword.resize(std::remove_if(keyword.begin(), keyword.end(), isGarbage) - keyword.begin());

    // Transform all characters to lower case
    std::transform(keyword.begin(), keyword.end(), keyword.begin(), ::tolower);

    // After filtering, if the keyword is empty or it is contained in stop words list, mark as invalid keyword
    if (keyword.size() == 0 || stopwords_.find(keyword) != stopwords_.end())
        return false;

    return true;
}

首先,这些函数(alls是STL容器或标准函数的成员函数)应该是快速的,并且在索引阶段不需要花费很多时间。但在与Valgrind进行分析之后,这个filter的包容性成本是荒谬的高:33.4%。此过滤器有三个标准函数占用该百分比的大部分时间:std::remove_if占6.53%,std::set::find占15.07%,std::transform占7.71%。

因此,如果有任何我可以做(或更改)以减少此过滤器的指令时间成本(如使用并行化或类似的东西),请给我你的建议。提前致谢。

更新:感谢你的所有建议。所以简而言之,我总结一下我需要做的是: 1)通过构造我自己的循环将tolowerremove_if合并为一个。 2)使用unordered_set代替set以获得更快的find方法。 因此,我选择Mark_B作为正确答案。

7 个答案:

答案 0 :(得分:2)

首先,您确定在编译时启用了优化和内联吗?

假设是这种情况,我首先尝试编写自己的变换器,将垃圾和低层套管组合成一个步骤,以防止第二次迭代关键字。

如果没有使用评论中建议的unordered_set之类的其他容器,则无法对此查找做很多事情。

您的应用程序是否有可能真正进行过滤只是CPU操作中的一部分?

答案 1 :(得分:2)

如果使用boost过滤器迭代器,则可以将remove_iftransform合并为一个,例如(未经测试):

keyword.erase(std::transform(boost::make_filter_iterator(!boost::bind(isGarbage), keyword.begin(), keyword.end()),
                             boost::make_filter_iterator(!boost::bind(isGarbage), keyword.end(), keyword.end()),
                             keyword.begin(),
                            ::tolower), keyword.end());

这假设您希望修改字符串的副作用仍然在外部可见,否则通过const引用而只需使用count_if和谓词来完成所有操作。您可以为停止词列表构建分层数据结构(基本上是树),使“就地”匹配成为可能,例如,如果您的停用词是SELECT, SELECTION, SELECTED,您可以构建树:

|- (other/empty accept)
\- S-E-L-E-C-T- (empty, fail)
             |- (other, accept)
             |- I-O-N (fail)
             \- E-D (fail)

您可以在转换和过滤的同时遍历这样的树结构,而无需对字符串本身进行任何修改。实际上,您需要将多字符运行压缩到树中的单个节点(可能)。

您可以通过以下方式轻松构建此类数据结构:

#include <iostream>
#include <map>
#include <memory>

class keywords {
  struct node {
        node() : end(false) {}
    std::map<char, std::unique_ptr<node>> children;
        bool end;
  } root;

  void add(const std::string::const_iterator& stop, const std::string::const_iterator c, node& n) {
    if (!n.children[*c])
      n.children[*c] = std::unique_ptr<node>(new node);

    if (stop == c+1) {
      n.children[*c]->end = true;
      return;
    }
    add(stop, c+1, *n.children[*c]);
  }
public:
  void add(const std::string& str) {
    add(str.end(), str.begin(), root);
  }

  bool match(const std::string& str) const {
    const node *current = &root;
    std::string::size_type pos = 0;
    while(current && pos < str.size()) {
      const std::map<char,std::unique_ptr<node>>::const_iterator it = current->children.find(str[pos++]);
      current = it != current->children.end() ? it->second.get() : nullptr;
    }
    if (!current) {
      return false;
    }
    return current->end;
  }
};

int main() {
  keywords list;
  list.add("SELECT");
  list.add("SELECTION");
  list.add("SELECTED");
  std::cout << list.match("TEST") << std::endl;
  std::cout << list.match("SELECT") << std::endl;
  std::cout << list.match("SELECTOR") << std::endl;
  std::cout << list.match("SELECTED") << std::endl;
  std::cout << list.match("SELECTION") << std::endl;
}

这就像你希望并给予的那样有效:

0
1
0
1
1

然后只需要修改match()来适当调用转换和过滤函数,例如:

const char c = str[pos++];
if (filter(c)) {
  const std::map<char,std::unique_ptr<node>>::const_iterator it = current->children.find(transform(c));
}

您可以对此进行一些优化(紧凑的长单字符串运行)并使其更通用,但它显示了如何在一次通过中就地执行所有操作,并且这是加速您显示的功能的最可能的候选者

(当然是基准变化)

答案 2 :(得分:1)

您可以通过单个传递字符串来更快地完成此操作,忽略垃圾字符。像这样的东西(伪代码):

std::string normalizedKeyword;
normalizedKeyword.reserve(keyword.size())
for (auto p = keyword.begin(); p != keyword.end(); ++p)
{
    char ch = *p;
    if (!isGarbage(ch))
        normalizedKeyword.append(tolower(ch));
}

// then search for normalizedKeyword in stopwords

这应该可以消除std::remove_if的开销,尽管存在内存分配以及将字符复制到normalizedKeyword的一些新开销。

答案 3 :(得分:1)

如果对isGarbage()的调用不需要同步,那么并行化应该是第一个要考虑的优化(当然,过滤一个关键字是一个足够大的任务,否则并行化应该高一级)。以下是如何完成的 - 一次通过原始数据,使用线程构建模块进行多线程:

    bool isGarbage(char c) {
    return c == 'a';
}

struct RemoveGarbageAndLowerCase {
    std::string result;
    const std::string& keyword;

    RemoveGarbageAndLowerCase(const std::string& keyword_) : keyword(keyword_) {}

    RemoveGarbageAndLowerCase(RemoveGarbageAndLowerCase& r, tbb::split) : keyword(r.keyword) {}

    void operator()(const tbb::blocked_range<size_t> &r) {
        for(size_t i = r.begin(); i != r.end(); ++i) {
            if(!isGarbage(keyword[i])) {
                result.push_back(tolower(keyword[i]));
            }
        }
    }

    void join(RemoveGarbageAndLowerCase &rhs) {
        result.insert(result.end(), rhs.result.begin(), rhs.result.end());
    }
};

void filter_garbage(std::string &keyword) {
    RemoveGarbageAndLowerCase res(keyword);
    tbb::parallel_reduce(tbb::blocked_range<size_t>(0, keyword.size()), res);
    keyword = res.result;
}

int main() {
    std::string keyword = "ThIas_iS:saome-aTYpe_Ofa=MoDElaKEYwoRDastrang";

    filter_garbage(keyword);

    std::cout << keyword << std::endl;

    return 0;
}

当然,最终的代码可以通过避免数据复制来进一步改进,但样本的目标是证明它是一个容易解决的问题。

答案 4 :(得分:0)

这里的问题不是标准功能,而是你对它们的使用。当你显然只需要做一个时,你就会对你的字符串进行多次传递。

您需要做的事情可能无法通过算法直接完成,您需要通过提升或滚动自己的帮助。

您还应该仔细考虑是否确实需要调整字符串大小。是的,你可能会节省一些空间,但这会让你付出代价。单独删除它可能会占用您的操作费用。

答案 5 :(得分:0)

这是一种将垃圾清除和下部套管组合成一个步骤的方法。它不适用于UTF-8等多字节编码,但原始代码也没有。我认为01都是垃圾值。

bool Indexer::filter(string &keyword)
{
    static char replacements[256] = {1}; // initialize with an invalid char
    if (replacements[0] == 1)
    {
        for (int i = 0;  i < 256;  ++i)
            replacements[i] = isGarbage(i) ? 0 : ::tolower(i);
    }
    string::iterator tail = keyword.begin();
    for (string::iterator it = keyword.begin();  it != keyword.end();  ++it)
    {
        unsigned int index = (unsigned int) *it & 0xff;
        if (replacements[index])
            *tail++ = replacements[index];
    }
    keyword.resize(tail - keyword.begin());

    // After filtering, if the keyword is empty or it is contained in stop words list, mark as invalid keyword
    if (keyword.size() == 0 || stopwords_.find(keyword) != stopwords_.end())
        return false;

    return true;
}

您的时间安排的最大部分是std::set::find,所以我也会尝试std::unordered_set看看它是否有所改善。

答案 6 :(得分:-1)

我会用较低级别的C函数实现它,这样的事情可能(不检查这个编译),在适当的位置进行替换而不调整关键字的大小。

  1. 我没有使用垃圾字符集,而是添加一个包含所有256个字符的静态表(是的,它仅适用于ascii),0表示所有可用的字符,1表示应该是过滤掉了。类似的东西:
  2. static const char GARBAGE[256] = { 1, 1, 1, 1, 1, ...., 0, 0, 0, 0, 1, 1, ... };

    然后对于pos中偏移量const char *str中的每个字符,您只需检查if (GARBAGE[str[pos]] == 1);

    这或多或少是无序集所做的,但指令却少得多。 stopwords如果不是,则应该是无序的。

    现在过滤功能(我假设ascii / utf8和null终止字符串):

    bool Indexer::filter(char *keyword)
    {
    
        char *head = pos;
        char *tail = pos;
    
        while (*head != '\0') {
            //copy non garbage chars from head to tail, lowercasing them while at it
            if (!GARBAGE[*head])  {
               *tail = tolower(*head);
               ++tail; //we only advance tail if no garbag
            }
            //head always advances
            ++head;
        }
        *tail = '\0';
    
        // After filtering, if the keyword is empty or it is contained in stop words list, mark as invalid keyword
        if (tail == keyword || stopwords_.find(keyword) != stopwords_.end())
            return false;
    
    
        return true;
    }