C++,Qt - 尽可能快地拆分QByteArray

use*_*186 5 c++ performance qt split qbytearray

我正在尝试将QByteArray包含UTF-8编码纯文本(使用空格作为分隔符)的大量文本拆分为最佳性能.我发现如果我将数组转换为QString第一个,我可以获得更好的结果.我尝试使用正则QString.split表达式使用该函数,但性能非常糟糕.这段代码变得更快:

QMutex mutex;
QSet<QString> split(QByteArray body)
{
    QSet<QString>  slova;

    QString s_body = QTextCodec::codecForMib(106)->toUnicode(body);
    QString current;

    for(int i = 0; i< body.size(); i++){
        if(s_body[i] == '\r' || s_body[i] == '\n' || s_body[i] == '\t' || s_body[i] == ' '){

            mutex.lock();
            slova.insert(current);

            mutex.unlock();
            current.clear();
            current.reserve(40);
        } else {
            current.push_back(s_body[i]);
        }
    }
    return slova;
}
Run Code Online (Sandbox Code Playgroud)

"Slova" QSet<QString>目前是,但我可以使用std::set或任何其他格式.此代码应该可以找到阵列中有多少个唯一的单词,并且可以获得最佳性能.

不幸的是,这段代码远远不够快.我希望从中挤出绝对最大值.

使用callgrind,我发现最贪婪的内部函数是:

QString::reallocData (18% absolute cost)
QString::append (10% absolute cost)
QString::operator= (8 % absolute cost)
QTextCodec::toUnicode (8% absolute cost)
Run Code Online (Sandbox Code Playgroud)

显然,这与源于该push_back函数的内存分配有关.解决这个问题的最佳方法是什么?不一定必须是Qt解决方案 - 纯C或C++也是可以接受的.

Tob*_*ght 3

最大限度地减少您需要执行的复印量。将输入缓冲区保留为 UTF-8,并且不要将std::stringor存储QString在您的集合中;相反,创建一个小类来引用现有的 UTF-8 数据:

#include <QString>

class stringref {
    const char *start;
    size_t length;

public:
    stringref(const char *start, const char *end);
    operator QString() const;
    bool operator<(const stringref& other) const;
};
Run Code Online (Sandbox Code Playgroud)

这可以封装 UTF-8 输入的子字符串。您需要确保它不会比输入字符串的寿命更长;您可以通过巧妙地使用 来做到这一点std::shared_ptr,但如果代码相当独立,那么它应该足够容易处理以推断生命周期。

我们可以从一对指向 UTF-8 数据的指针构造它,并QString在我们想要实际使用它时将其转换为:

stringref::stringref(const char *start, const char *end)
    : start(start), length(end-start)
{}

stringref::operator QString() const
{
    return QString::fromUtf8(start, length);
}
Run Code Online (Sandbox Code Playgroud)

您需要定义operator<以便可以在std::set.

#include <cstring>
bool stringref::operator<(const stringref& other) const
{
    return length == other.length
        ? std::strncmp(start, other.start, length) < 0
        : length < other.length;
}
Run Code Online (Sandbox Code Playgroud)

请注意,我们在取消引用指针之前按长度排序,以减少缓存影响。


现在我们可以编写split方法:

#include <set>
#include <QByteArray>
std::set<stringref> split(const QByteArray& a)
{
    std::set<stringref> words;

    // start and end
    const auto s = a.data(), e = s + a.length();

    // current word
    auto w = s;

    for (auto p = s;  p <= e;  ++p) {
        switch (*p) {
        default: break;
        case ' ': case '\r': case '\n': case '\t': case '\0':
            if (w != p)
                words.insert({w, p});
            w = p+1;
        }
    }

    return words;
}
Run Code Online (Sandbox Code Playgroud)

该算法几乎是您自己的,添加了测试w!=p,这样就不会计算空白的运行。


让我们测试一下,并计算重要部分的时间:

#include <QDebug>
#include <chrono>
int main()
{
    QByteArray body{"foo bar baz\n  foo again\nbar again "};
    // make it a million times longer
    for (int i = 0;  i < 20;  ++i)
        body.append(body);

    using namespace std::chrono;
    const auto start = high_resolution_clock::now();

    auto words = split(body);

    const auto end = high_resolution_clock::now();
    qDebug() << "Split"
             << body.length()
             << "bytes in"
             << duration_cast<duration<double>>(end - start).count()
             << "seconds";

    for (auto&& word: words)
        qDebug() << word;
}
Run Code Online (Sandbox Code Playgroud)

我得到:

在 1.99142 秒内分割 35651584 字节
“bar”
“baz”
“foo”
“again”

编译-O3时间减少到 0.6188 秒,所以不要忘记向编译器寻求帮助!

如果这仍然不够快,那么可能是时候开始考虑并行化任务了。您需要将字符串拆分为大致相等的长度,但前进到下一个空白,以便没有工作跨越两个线程的工作。每个线程应该创建自己的结果集,然后缩减步骤是合并结果集。我不会为此提供完整的解决方案,因为这本身就是另一个问题。