Simplify orPostingIterator and make it faster
ClosedPublic

Authored by bruns on Mar 31 2018, 4:18 AM.

Details

Summary

Trivial searches (e.g. baloosearch foo) are expanded to a large list
of ORed small results sets (e.g. 80 terms with 2...5 entries), thus
speeding it up is quite beneficial.

Currently, iterators which no longer return any entries are deleted
and replaced with nullptrs, thus the value has to be checked on each
iteration. The saved instructions are sufficient to more than amortize
the cost of moving the remaining elements in the vector.

The or operator has to return the smallest ID of the combined sets.
Instead of doing a traversal on each next() call, determine the smallest
ID on the first call and update it when checking if the iterators have
to be advanced.

Keep the docId in a local variable, as the virtual function call to
(PostingIterator*)->docId() is somewhat expensive.

According to valgrind, typical execution cost of Baloo::Query::exec()
is reduced by 25% to 40%.

Signed-off-by: Stefan Brüns <stefan.bruens@rwth-aachen.de>

Test Plan

valgrind --tool=callgrind baloosearch foo OR bar

Diff Detail

Repository
R293 Baloo
Branch
speedup_orpostingoperator
Lint
No Linters Available
Unit
No Unit Test Coverage
bruns created this revision.Mar 31 2018, 4:18 AM
Restricted Application added projects: Frameworks, Baloo. · View Herald TranscriptMar 31 2018, 4:18 AM
Restricted Application added a subscriber: Frameworks. · View Herald Transcript
bruns requested review of this revision.Mar 31 2018, 4:18 AM
bruns added inline comments.Apr 6 2018, 6:57 PM
src/engine/orpostingiterator.cpp
44 ↗(On Diff #31008)

Move this to the constructor?

47 ↗(On Diff #31008)

iterator may contain nullptr's

bruns updated this revision to Diff 31623.Apr 7 2018, 7:33 PM
bruns added a reviewer: michaelh.

cleanup

michaelh added a comment.EditedApr 8 2018, 7:37 AM

While reading in IDE realized (*it) resolves to PostingIterator** I got a little dizzy at first, then started to play with this code and came up with this.
In Constructor:

for (PostingIterator* it : m_iterators) {
  if (it == nullptr) {
    m_iterators.erase(&it);
   } else {
     auto docId = it->next();
     if (docId && (docId < m_nextId || m_nextId == 0)) {
       m_nextId = docId;
     }
  }
}

In OrPostingIterator::next()

for (PostingIterator* it : m_iterators) {
    auto docId = it->docId();
    if (docId <= m_docId) {
        docId = it->next();
        if (docId == 0) {
            m_iterators.erase(&it);
            continue;
        }
    }
    m_nextId = m_nextId ? qMin(docId , m_nextId) : docId;
}

I'm not 100% sure if this does exactly the same. But if it does, for readers of my skill level this code will be easier to understand, I guess.
BTW, &it also makes me dizzy :-)

src/engine/orpostingiterator.cpp
28

, m_nextId(ULONG_LONG_MAX) ?

44

m_nextId = (docId > 0) ? qMin(m_nextId, docId) : m_nextId; ?

68

Remove the above and instead ?

if (m_nextId == ULONG_LONG_MAX) {
  m_docId = 0;
  return 0;
} 
m_docId = m_nextId;

?

71

Newbie question: Does std::unique_ptr make sense here?

88

m_nextId = qMin(docId, m_nextId); ?

I'm not very familiar with the concept of iterators (yet). To me it looks like auto i = new OrPostingIterator(iters); i->docId(); will return 0 and i->next() returns a valid docId. After that i->docId(); is also valid.
Is this how iterators work? Naively I would expect i->docId(); to be valid after construction or both i->docId(); and i->next() to be invalid.

I have to admit, it's very difficult to understand (and I don't, really) what baloo is doing here with all this iterating over vectors of inherited iterator classes, pointer-to-pointer stuff of which some might not even exist. My brain hurts, terribly! All I can presently do is ask some simple questions. Sorry, I'm of no help here. Please take my comments as a device for better understanding and I hope it's not to much of a bother.

bruns added a comment.Apr 9 2018, 3:42 PM

While reading in IDE realized (*it) resolves to PostingIterator** I got a little dizzy at first, then started to play with this code and came up with this.
In Constructor:

for (PostingIterator* it : m_iterators) {
  if (it == nullptr) {
    m_iterators.erase(&it);

Thats not how QVector<>::iterator works

  • you can not create an QVector<>::iterator from an PostingIterator*. The latter is just a (pointer to) a PostingIterator, it does not know about the container (QVector) it is member of [1]
  • deleting an element from a container using .erase(it) typically invalidates the it, thats the reason .erase(it) returns an iterator to the next element
  • it = m_iterators.erase(it) thus implicitly moves the iterator forward
 } else {
   auto docId = it->next();
   if (docId && (docId < m_nextId || m_nextId == 0)) {
     m_nextId = docId;
   }
}

}

In `OrPostingIterator::next()`

does not work for the same reason ...

What could work is deleting the PostingIterator immediately and setting the iterator value to nullptr, and then do a cleanup pass afterwards with QVector<>::removeAll(nullptr).

bruns added inline comments.Apr 9 2018, 3:47 PM
src/engine/orpostingiterator.cpp
71

When writing from scratch today, yes.
But this would not disallow nullptr (std::make_unique<PostingIterator>(nullptr) is fully valid)

:-) Thank you for the lesson.

bruns added a comment.Apr 9 2018, 5:06 PM

I'm not very familiar with the concept of iterators (yet). To me it looks like auto i = new OrPostingIterator(iters); i->docId(); will return 0 and i->next() returns a valid docId. After that i->docId(); is also valid.
Is this how iterators work? Naively I would expect i->docId(); to be valid after construction or both i->docId(); and i->next() to be invalid.

I have to admit, it's very difficult to understand (and I don't, really) what baloo is doing here with all this iterating over vectors of inherited iterator classes, pointer-to-pointer stuff of which some might not even exist. My brain hurts, terribly! All I can presently do is ask some simple questions. Sorry, I'm of no help here. Please take my comments as a device for better understanding and I hope it's not to much of a bother.

Each PostingIterator represents a "set" of document ids. PostingIterators are not related to STL or Qt container iterators (although the "subiterators" are implemented using QVectors<>).

The PI::next() call returns the smallest element and removes it from the set, PI::docId() just returns the smallest element.

The result set of the OrPostingIterator is the union of its "subsets" (represented by its "subiterators", i.e. the QVector<PostingIterator*> constructor argument). Or works a little bit like an insertion sort. To return the smallest element, it creates the union of the smallest element of all subsets, and yields the smallest element of the union. It then calls PI::next() on all subsets which returned the smallest element, removing it from all subsets and thus from the union.

In C++, you can use iterators in two different ways, either PreIncrement or PostIncrement. PostingIterator::next() will behave like *(++it), i.e. it is incremented first, then the current value is returned.

I don't know the original justification for this design. One possible idea is to delay the actual database query until next() is called the first time. For e.g. a query "a AND b AND c", one can execute "a", if not empty do "b", intersect (i.e. calling the Iterators of Result("a") and Result("b") until both return the same value), and if the intersection is non-empty, do "c".

Currently, it does not implement this optimization. D12025 implements one case, i.e. stopping if a leaf query returns an empty set, and propagating it upwards.

Thank you once more. This is complicated stuff (for me), it will take some time to sink in.

bruns marked 8 inline comments as done.Apr 21 2018, 5:48 PM
bruns added inline comments.
src/engine/orpostingiterator.cpp
28

Currently the only reserved value is 0, and I prefer to keep it as is.

bruns marked an inline comment as done.Jun 26 2018, 10:31 PM

Can someone please review this change?

Restricted Application added a subscriber: kde-frameworks-devel. · View Herald TranscriptJun 26 2018, 10:31 PM
fvogt added a subscriber: fvogt.Jun 28 2018, 5:28 PM
fvogt added inline comments.
src/engine/orpostingiterator.cpp
86

Looks like most of those parens are unnecessary.

bruns added inline comments.Jun 29 2018, 11:28 PM
src/engine/orpostingiterator.cpp
86

only one pair is, the others support readability, see Qt Coding Style:

Use parentheses to group expressions

bruns updated this revision to Diff 36944.Jun 30 2018, 1:06 PM

remove extra parentheses

bruns marked an inline comment as done.Aug 18 2018, 4:36 PM

Ping!

bruns removed a reviewer: michaelh.Aug 18 2018, 4:37 PM

Looks fine to me. But do we really need to optimize it? I mean, I didn't see it running more than ~20 ms, and with this patch for small queries it runs like ~16 ms. Worst case is when user types something in KRunner, but again, the lag is negligible there.

src/engine/orpostingiterator.cpp
57–58 ↗(On Diff #31008)

Usage of auto keyword (at least for docId) somehow made it a bit harder to read for me. Looks like Qt Coding Conventions agrees
(it's fine for iterators, whose definition has a long-long-name and it's clear from the line that it's an iterator. But for ordinary docId I've began to think it's not quint64, but some weird class with large definition or something).

63 ↗(On Diff #31008)

I'm not really sure, do we actually need to set it to nullptr? It becomes inaccessible after next line

bruns added a comment.Oct 8 2018, 9:32 PM

Looks fine to me. But do we really need to optimize it? I mean, I didn't see it running more than ~20 ms, and with this patch for small queries it runs like ~16 ms. Worst case is when user types something in KRunner, but again, the lag is negligible there.

KRunner currently executes each search term 8 times, once for each of type in [ Audio, Video, Document, ...]. I think especially for krunner lower latency is important, as it does search-as-you-type.

src/engine/orpostingiterator.cpp
63 ↗(On Diff #31008)

It makes debugging a little bit nicer, erase is implemented as swap, and when setting it the vector only contains nullptrs after size() elements.

bruns updated this revision to Diff 53527.Mar 9 2019, 4:33 PM

rebase

bruns marked 5 inline comments as done.Apr 11 2019, 6:37 PM

Ping ...

ngraham accepted this revision.Apr 11 2019, 8:47 PM
ngraham added a subscriber: ngraham.

Let's get this in.

This revision is now accepted and ready to land.Apr 11 2019, 8:47 PM
This revision was automatically updated to reflect the committed changes.