簡體   English   中英

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

[英]C++, Qt - splitting a QByteArray as fast as possible

我正在嘗試拆分包含UTF-8編碼純文本(使用空格作為分隔符)的大量QByteArray ,以盡可能提高性能。 我發現如果我先將數組轉換為QString ,我可以獲得更好的結果。 我嘗試使用正QString.split表達式使用QString.split函數,但性能非常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;
}

“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)

顯然,這與源自push_back函數的內存分配有關。 解決這個問題的最佳方法是什么? 不一定必須是Qt解決方案 - 純C或C ++也是可以接受的。

如果我是你,我要做的第一件事是修改你的代碼,這樣就不會鎖定和解鎖QMutex,因為它會插入到QSet中 - 這是純粹的開銷。 在循環開始時僅鎖定QMutex一次,並在循環終止后再次解鎖; 或者更好的是,插入到無法從任何其他線程訪問的QSet中,這樣您根本不需要鎖定任何QMutex。

除此之外,要做的第二件事是盡可能多地消除堆分配。 理想情況下,您可以執行整個解析,而無需分配或釋放任何動態內存; 我的實現在下面做了(好吧,差不多 - unordered_set 可能會做一些內部分配,但可能不會)。 在我的計算機(2.7GHz Mac Mini)上,我使用Moby Dick的Gutenberg ASCII文本作為我的測試輸入,測量的處理速度大約為每秒1100萬字。

請注意,由於UTF-8使用的向后兼容編碼,此程序與UTF-8或ASCII輸入同樣適用。

#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include <unordered_set>

// Loads in a text file from disk into an in-memory array
// Expected contents of the file are ASCII or UTF8 (doesn't matter which).
// Note that this function appends a space to the end of the returned array
// That way the parsing function doesn't have to include a special case
// since it is guaranteed that every word in the array ends with whitespace
static char * LoadFile(const char * fileName, unsigned long * retArraySizeBytes)
{
   char * ret = NULL;
   *retArraySizeBytes = 0;

   FILE * fpIn = fopen(fileName, "r");
   if (fpIn)
   {
      if (fseek(fpIn, 0L, SEEK_END) == 0)
      {
         const unsigned long fileSizeBytes  = ftell(fpIn);
         const unsigned long arraySizeBytes = *retArraySizeBytes = fileSizeBytes+1;  // +1 because I'm going to append a space to the end
         rewind(fpIn);

         ret = new char[arraySizeBytes];
         if (fread(ret, 1, fileSizeBytes, fpIn) == fileSizeBytes)
         {
            ret[fileSizeBytes] = ' ';  // appending a space allows me to simplify the parsing step
         }
         else
         {
            perror("fread");
            delete [] ret;
            ret = NULL;
         }
      }
      else perror("fseek");

      fclose(fpIn);
   }
   return ret;
}

// Gotta provide our own equality-testing function otherwise unordered_set will just compare pointer values
struct CharPointersEqualityFunction : public std::binary_function<char *, char *,bool>
{  
    bool operator() (char * s1, char * s2) const {return strcmp(s1, s2) == 0;}
};

// Gotta provide our own hashing function otherwise unordered_set will just hash the pointer values
struct CharPointerHashFunction
{
   int operator() (char * str) const
   {
      // djb2 by Dan Bernstein -- fast enough and simple enough
      unsigned long hash = 5381;
      int c; while((c = *str++) != 0) hash = ((hash << 5) + hash) + c;
      return (int) hash;
   }
};

typedef std::unordered_set<char *, CharPointerHashFunction, CharPointersEqualityFunction > CharPointerUnorderedSet;

int main(int argc, char ** argv)
{
   if (argc < 2)
   {
      printf("Usage:  ./split_words filename\n");
      return 10;
   }    

   unsigned long arraySizeBytes;
   char * buf = LoadFile(argv[1], &arraySizeBytes);
   if (buf == NULL)
   {
      printf("Unable to load input file [%s]\n", argv[1]);
      return 10;
   }

   CharPointerUnorderedSet set;
   set.reserve(100000);  // trying to size (set) big enough that no reallocations will be necessary during the parse

   struct timeval startTime;
   gettimeofday(&startTime, NULL);

   // The actual parsing of the text is done here
   int wordCount = 0;
   char * wordStart = buf;
   char * wordEnd   = buf;
   char * bufEnd    = &buf[arraySizeBytes];
   while(wordEnd < bufEnd)
   {
      if (isspace(*wordEnd))
      {
         if (wordEnd > wordStart)
         {
            *wordEnd = '\0';
            set.insert(wordStart);
            wordCount++;
         }
         wordStart = wordEnd+1;   
      }
      wordEnd++;
   }

   struct timeval endTime;
   gettimeofday(&endTime, NULL);

   unsigned long long startTimeMicros = (((unsigned long long)startTime.tv_sec)*1000000) + startTime.tv_usec;
   unsigned long long endTimeMicros   = (((unsigned long long)  endTime.tv_sec)*1000000) + endTime.tv_usec;
   double secondsElapsed = ((double)(endTimeMicros-startTimeMicros))/1000000.0;

   printf("Parsed %i words (%zu unique words) in %f seconds, aka %.0f words/second\n", wordCount, set.size(), secondsElapsed, wordCount/secondsElapsed);
   //for (const auto& elem: set) printf("word=[%s]\n", elem);

   delete [] buf;
   return 0;
}

可疑的最大成本是push_back導致頻繁的重新分配,因為你一次追加一個字符。 為什么不提前搜索,然后使用QString::mid()一次附加所有數據:

slova.insert(s_body.mid(beginPos, i - beginPos - 1));

其中beginPos保存當前子字符串開頭的索引。 在將每個字符插入slova之前,不是將每個字符附加到current字符,而是立即復制所有字符。 復制子字符串后,提前搜索下一個有效 (不是分隔符)字符,並將beginPos設置beginPos等於該索引。

在(粗略)代碼中:

QString s_body = ...
//beginPos tells us the index of the current substring we are working 
//with. -1 means the previous character was a separator
int beginPos = -1;
for (...) {
    //basically your if statement provided in the question as a function
    if (isSeparator(s_body[i])) {
         //ignore double white spaces, etc.
         if (beginPos != -1) {
             mutex.lock();
             slova.insert(s_body.mid(beginPos, i - beginPos - 1));
             mutex.unlock();
         }
    } else if (beginPos == -1)
        //if beginPos is not valid and we are not on a separator, we 
        //are at the start of a new substring.
         beginPos = i;
}

這種方法將大大減少堆分配的開銷,並消除QString::push_back()調用。

最后一點: QByteArray還提供了mid()函數。 您可以完全跳過轉換為QString並直接使用字節數組。

最大限度地減少您需要執行的復制量。 將輸入緩沖區保持為UTF-8,並且不要在集合中存儲std::stringQString ; 相反,創建一個小類來引用現有的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;
};

這可以封裝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);
}

您需要定義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;
}

請注意,我們在取消引用指針之前按長度排序,以減少緩存影響。


現在我們可以編寫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;
}

該算法幾乎是你的,增加了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;
}

我明白了:

在1.99142秒內拆分35651584字節
“酒吧”
“巴茲”
“富”
“再次”

使用-O3編譯將時間減少到0.6188秒,所以不要忘記向編譯器尋求幫助!

如果仍然不夠快,可能是時候開始考慮並行化任務了。 你需要將字符串拆分成大致相等的長度,但是要前進到下一個空格,這樣就沒有任何工作跨越兩個線程值得工作。 每個線程都應創建自己的一組結果,然后還原步驟將合並結果集。 我不會為此提供完整的解決方案,因為這本身就是另一個問題。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM