迭代行时如何只保留最后一个副本

时间:2009-10-30 14:24:29

标签: c++

以下代码遍历许多数据行,每行计算一些分数,然后根据该分数对行进行排序:

unsigned count = 0;
score_pair* scores = new score_pair[num_rows];
while ((row = data.next_row())) {
    float score = calc_score(data.next_feature())
    scores[count].score = score;
    scores[count].doc_id = row->docid;
    count++;
}

assert(count <= num_rows);
qsort(scores, count, sizeof(score_pair), score_cmp);

不幸的是,有许多重复的行具有相同的docid但得分不同。现在我想保留任何docid的最后得分。 docids是unsigned int,但通常很大(=&gt;没有查找数组) - 使用HashMap查找docid的最后一次计数可能太慢了(数百万行,只需要几秒钟而不是几分钟...... )。

好的,我修改了我的代码以使用std:map:

map<int, int> docid_lookup;
unsigned count = 0; 
score_pair* scores = new score_pair[num_rows];
while ((row = data.next_row())) {
    float score = calc_score(data.next_feature())

    map<int, int>::iterator iter;
    iter = docid_lookup.find(row->docid);
    if (iter != docid_lookup.end()) {
        scores[iter->second].score = score;
        scores[iter->second].doc_id = row->docid;
    } else {
        scores[count].score = score;
        scores[count].doc_id = row->docid;
        docid_lookup[row->docid] = count;
        count++;
    }
}

它的工作原理和性能的打击并不像我预期的那么糟糕 - 现在它运行一分钟而不是16秒,所以它大约是3倍。内存使用率也从1Gb上升到4Gb。

5 个答案:

答案 0 :(得分:3)

我要尝试的第一件事就是mapunordered_map:如果性能比没有任何独特化处理的情况慢60倍,我会感到惊讶。如果表现不可接受,另一种选择是这样的:

// get the computed data into a vector
std::vector<score_pair>::size_type count = 0;
std::vector<score_pair> scores;
scores.reserve(num_rows);
while ((row = data.next_row())) {
    float score = calc_score(data.next_feature())
    scores.push_back(score_pair(score, row->docid));
}

assert(scores.size() <= num_rows);

// remove duplicate doc_ids
std::reverse(scores.begin(), scores.end());
std::stable_sort(scores.begin(), scores.end(), docid_cmp);
scores.erase(
    std::unique(scores.begin(), scores.end(), docid_eq),
    scores.end()
);

// order by score
std::sort(scores.begin(), scores.end(), score_cmp);

请注意,使用reverse和stable_sort是因为您需要每个doc_id的最后得分,但std :: unique保留第一个得分。如果你想获得第一个分数,你可以使用stable_sort,如果你不关心什么分数,你可以使用sort。

处理此问题的最佳方法可能是将反向迭代器传递给std :: unique,而不是单独的反向操作。但是我没有信心我可以在没有测试的情况下正确编写,错误可能会让人感到困惑,所以你得到了未经优化的代码...

编辑:只是为了与您的代码进行比较,以下是我如何使用地图:

std::map<int, float> scoremap;
while ((row = data.next_row())) {
    scoremap[row->docid] = calc_score(data.next_feature());
}
std::vector<score_pair> scores(scoremap.begin(), scoremap.end());
std::sort(scores.begin(), scores.end(), score_cmp);

请注意,score_pair需要一个构造函数取std::pair<int,float>,这使得它不是POD。如果这是不可接受的,请使用std :: transform,并使用函数进行转换。

最后,如果存在大量重复(例如,每个doc_id平均有2个或更多条目),并且如果calc_score不重要,那么我将查看是否可以迭代{{1}的行以相反的顺序。如果是,那么它将加快data / map方法,因为当您获得doc_id的命中时,您不需要计算该行的分数,只需删除它并移动上。

答案 1 :(得分:2)

我要去std :: docids地图。如果您可以创建适当的散列函数,则最好使用散列映射。但我想这太难了。并且没有 - std :: map不会太慢。访问是O(log n),几乎和O(1)一样好。 O(1)是数组访问时间(和Hashmap btw)。

顺便说一下,如果std :: map太慢,qsort O(n log n)也太慢了。并且,使用std :: map并迭代它的内容,你可以保存你的qsort。


评论的一些补充(bybyone):

  • 我没有去实施 细节,因为还不够 有关的信息。
  • qsort可能会因排序数据而表现不佳 (取决于实施)。 Std :: map可能没有。这是真实的 优点,特别是如果你看过 来自数据库的值可能 输出按键排序。
  • 内存分配策略没有任何说法。通过快速分配小对象来更改为内存分配器可以提高性能。
  • 仍然 - 最快的是具有适当散列函数的哈希映射。由于关于密钥分发的信息不足,因此无法在此答案中提供一个。

简短 - 如果你问一般性问题,你会得到一般答案。这意味着 - 至少对我来说,看一下O-Notation的时间复杂度。仍然你是对的,根据不同的因素,std :: map可能太慢而qsort仍然足够快 - 在qsort的最坏情况下它也可能是另一种方式,它具有n ^ 2的复杂性。 / p>

答案 2 :(得分:1)

除非我误解了这个问题,否则解决方案可以大大简化。至少据我所知,你有几百万个docid(类型为unsigned int),对于每个唯一的docid,你想要存储一个'得分'(这是一个浮点数)。如果输入中同一个docid多次出现,您希望保留最后一个得分。如果这是正确的,代码可以简化为:

std::map<unsigned, float> scores;

while ((row = data.next_row())) 
    scores[row->docid] = calc_score(data.next_feature());

这可能比原始版本慢一些,因为它分配了很多单独的块而不是一块大的内存块。鉴于你的声明,docid中有很多重复,我希望这可以节省相当多的内存,因为它只存储每个唯一docid的数据而不是原始数据中的每一行。

如果你想优化它,你几乎肯定会这样做 - 因为它使用了很多小块,为此目的设计的自定义分配器可能会有所帮助。一种可能性是看看Andrei Alexandrescu的Loki图书馆中的小块分配器。从那以后他就这个问题做了更多的工作,但是Loki中的那个可能足以完成手头的任务 - 它几乎肯定会节省相当多的内存并且运行得更快。

答案 3 :(得分:1)

如果你的C ++实现有,并且大多数都有,请尝试hash_map而不是std::map(有时在std::hash_map下可用)。

如果查找本身是您的计算瓶颈,那么这可能比std::map的二叉树显着加速。

答案 4 :(得分:0)

为什么不首先按doc id排序,计算得分,然后对于重复项的任何子集使用最高得分?

重新阅读问题;我建议对如何读入分数进行修改。请记住,C ++不是我的母语,所以这不太可编译。

unsigned count = 0;
pair<int, score_pair>* scores = new pair<int, score_pair>[num_rows];
while ((row = data.next_row())) {
    float score = calc_score(data.next_feature())
    scores[count].second.score = score;
    scores[count].second.doc_id = row->docid;
    scores[count].first = count;
    count++;
}

assert(count <= num_rows);
qsort(scores, count, sizeof(score_pair), pair_docid_cmp);

//getting number of unique scores
int scoreCount = 0;
for(int i=1; i<num_rows; i++) 
    if(scores[i-1].second.docId != scores[i].second.docId) scoreCount++; 
score_pair* actualScores=new score_pair[scoreCount];

int at=-1;
int lastId = -1;
for(int i=0; i<num_rows; i++)
{ 
    //if in first entry of new doc id; has the last read time by pair_docid_cmp
    if(lastId!=scores[i].second.docId) 
        actualScores[++at]=scores[i].second;
}
qsort(actualScores, count, sizeof(score_pair), score_cmp);

pair_docid_cmp首先在docid上进行比较;将相同的文档分组在一起,然后按逆序读取;这样读取的最后一项是具有相同docid的项目子列表中的第一项。应该只有~5 / 2x内存使用量,并且〜执行速度加倍。