Files
simdjson-simdjson/include/simdjson/dom/document_stream.h
T
Daniel Lemire 4dfbf98e4e Using a worker instead of a thread per batch (#920)
In the parse_many function, we have one thread doing the stage 1, while the main thread does stage 2. So if stage 1 and stage 2 take half the time, the parse_many could run at twice the speed. It is unlikely to do so. Still, we see benefits of about 40% due to threading.

To achieve this interleaving, we load the data in batches (blocks) of some size. In the current code (master), we create a new thread for each batch. Thread creation is expensive so our approach only works over sizeable batches. This PR improves things and makes parse_many faster when using small batches.

  This fixes our parse_stream benchmark which is just busted.
  This replaces the one-thread per batch routine by a worker object that reuses the same thread. In benchmarks, this allows us to get the same maximal speed, but with smaller processing blocks. It does not help much with larger blocks because the cost of the thread create gets amortized efficiently.
This PR makes parse_many beneficial over small datasets. It also makes us less dependent on the thread creation time.

Unfortunately, it is going to be difficult to say anything definitive in general. The cost of creating a thread varies widely depending on the OS. On some systems, it might be cheap, in others very expensive. It should be expected that the new code will depend less drastically on the performances of the underlying system, since we create juste one thread.

Co-authored-by: John Keiser <john@johnkeiser.com>
Co-authored-by: Daniel Lemire <lemire@gmai.com>
2020-06-12 16:51:18 -04:00

213 lines
6.4 KiB
C++

#ifndef SIMDJSON_DOCUMENT_STREAM_H
#define SIMDJSON_DOCUMENT_STREAM_H
#include "simdjson/common_defs.h"
#include "simdjson/dom/parser.h"
#include "simdjson/error.h"
#ifdef SIMDJSON_THREADS_ENABLED
#include <thread>
#include <mutex>
#include <condition_variable>
#endif
namespace simdjson {
namespace dom {
#ifdef SIMDJSON_THREADS_ENABLED
struct stage1_worker {
stage1_worker() noexcept = default;
stage1_worker(const stage1_worker&) = delete;
stage1_worker(stage1_worker&&) = delete;
stage1_worker operator=(const stage1_worker&) = delete;
~stage1_worker();
/**
* We only start the thread when it is needed, not at object construction, this may throw.
* You should only call this once.
**/
void start_thread();
/**
* Start a stage 1 job. You should first call 'run', then 'finish'.
* You must call start_thread once before.
*/
void run(document_stream * ds, dom::parser * stage1, size_t next_batch_start);
/** Wait for the run to finish (blocking). You should first call 'run', then 'finish'. **/
void finish();
private:
/**
* Normally, we would never stop the thread. But we do in the destructor.
* This function is only safe assuming that you are not waiting for results. You
* should have called run, then finish, and be done.
**/
void stop_thread();
std::thread thread{};
/** These three variables define the work done by the thread. **/
dom::parser * stage1_thread_parser{};
size_t _next_batch_start{};
document_stream * owner{};
/**
* We have two state variables. This could be streamlined to one variable in the future but
* we use two for clarity.
*/
bool has_work{false};
bool can_work{true};
/**
* We lock using a mutex.
*/
std::mutex locking_mutex{};
std::condition_variable cond_var{};
};
#endif
/**
* A forward-only stream of documents.
*
* Produced by parser::parse_many.
*
*/
class document_stream {
public:
/** Move one document_stream to another. */
really_inline document_stream(document_stream && other) noexcept = default;
really_inline ~document_stream() noexcept;
/**
* An iterator through a forward-only stream of documents.
*/
class iterator {
public:
/**
* Get the current document (or error).
*/
really_inline simdjson_result<element> operator*() noexcept;
/**
* Advance to the next document.
*/
inline iterator& operator++() noexcept;
/**
* Check if we're at the end yet.
* @param other the end iterator to compare to.
*/
really_inline bool operator!=(const iterator &other) const noexcept;
private:
really_inline iterator(document_stream &s, bool finished) noexcept;
/** The document_stream we're iterating through. */
document_stream& stream;
/** Whether we're finished or not. */
bool finished;
friend class document_stream;
};
/**
* Start iterating the documents in the stream.
*/
really_inline iterator begin() noexcept;
/**
* The end of the stream, for iterator comparison purposes.
*/
really_inline iterator end() noexcept;
private:
document_stream &operator=(const document_stream &) = delete; // Disallow copying
document_stream(document_stream &other) = delete; // Disallow copying
/**
* Construct a document_stream. Does not allocate or parse anything until the iterator is
* used.
*/
really_inline document_stream(
dom::parser &parser,
const uint8_t *buf,
size_t len,
size_t batch_size,
error_code error = SUCCESS
) noexcept;
/**
* Parse the first document in the buffer. Used by begin(), to handle allocation and
* initialization.
*/
inline void start() noexcept;
/**
* Parse the next document found in the buffer previously given to document_stream.
*
* The content should be a valid JSON document encoded as UTF-8. If there is a
* UTF-8 BOM, the caller is responsible for omitting it, UTF-8 BOM are
* discouraged.
*
* You do NOT need to pre-allocate a parser. This function takes care of
* pre-allocating a capacity defined by the batch_size defined when creating the
* document_stream object.
*
* The function returns simdjson::EMPTY if there is no more data to be parsed.
*
* The function returns simdjson::SUCCESS (as integer = 0) in case of success
* and indicates that the buffer has successfully been parsed to the end.
* Every document it contained has been parsed without error.
*
* The function returns an error code from simdjson/simdjson.h in case of failure
* such as simdjson::CAPACITY, simdjson::MEMALLOC, simdjson::DEPTH_ERROR and so forth;
* the simdjson::error_message function converts these error codes into a string).
*
* You can also check validity by calling parser.is_valid(). The same parser can
* and should be reused for the other documents in the buffer.
*/
inline void next() noexcept;
/**
* Pass the next batch through stage 1 and return when finished.
* When threads are enabled, this may wait for the stage 1 thread to finish.
*/
inline void load_batch() noexcept;
/** Get the next document index. */
inline size_t next_batch_start() const noexcept;
/** Pass the next batch through stage 1 with the given parser. */
inline error_code run_stage1(dom::parser &p, size_t batch_start) noexcept;
dom::parser &parser;
const uint8_t *buf;
const size_t len;
const size_t batch_size;
size_t batch_start{0};
/** The error (or lack thereof) from the current document. */
error_code error;
#ifdef SIMDJSON_THREADS_ENABLED
inline void load_from_stage1_thread() noexcept;
/** Start a thread to run stage 1 on the next batch. */
inline void start_stage1_thread() noexcept;
/** Wait for the stage 1 thread to finish and capture the results. */
inline void finish_stage1_thread() noexcept;
/** The error returned from the stage 1 thread. */
error_code stage1_thread_error{UNINITIALIZED};
/** The thread used to run stage 1 against the next batch in the background. */
friend struct stage1_worker;
std::unique_ptr<stage1_worker> worker{new(std::nothrow) stage1_worker()};
/**
* The parser used to run stage 1 in the background. Will be swapped
* with the regular parser when finished.
*/
dom::parser stage1_thread_parser{};
#endif // SIMDJSON_THREADS_ENABLED
friend class dom::parser;
}; // class document_stream
} // namespace dom
} // namespace simdjson
#endif // SIMDJSON_DOCUMENT_STREAM_H