Support caller-provided memory for CompressionContext Callers that must avoid heap allocation entirely can now construct a CompressionContext over a caller-provided workspace of WorkspaceSize() bytes. Internally, WorkingMemory gains a non-allocating constructor laying out its scratch space in a caller-provided buffer, and RequiredSize() factoring out the size computation its allocating constructor already performed.
diff --git a/snappy-internal.h b/snappy-internal.h index f68ab77..582e886 100644 --- a/snappy-internal.h +++ b/snappy-internal.h
@@ -137,8 +137,16 @@ class WorkingMemory { public: explicit WorkingMemory(size_t input_size); + + // Non-allocating: lays out the scratch space in the caller-provided + // buffer, which must be at least RequiredSize(input_size) bytes, aligned + // at least as strictly as uint16_t, and must outlive "*this". + WorkingMemory(size_t input_size, char* buffer); ~WorkingMemory(); + // The buffer size required by the non-allocating constructor above. + static size_t RequiredSize(size_t input_size); + // Allocates and clears a hash table using memory in "*this", // stores the number of buckets in "*table_size" and returns a pointer to // the base of the hash table. @@ -149,6 +157,7 @@ private: char* mem_; // the allocated memory, never nullptr size_t size_; // the size of the allocated memory, never 0 + bool owns_mem_; // whether the destructor should free mem_ uint16_t* table_; // the pointer to the hashtable char* input_; // the pointer to the input scratch buffer char* output_; // the pointer to the output scratch buffer
diff --git a/snappy.cc b/snappy.cc index efca3af..69cc209 100644 --- a/snappy.cc +++ b/snappy.cc
@@ -76,6 +76,7 @@ #include <cstring> #include <limits> #include <memory> +#include <new> #include <string> #include <utility> #include <vector> @@ -757,19 +758,36 @@ } // namespace namespace internal { -WorkingMemory::WorkingMemory(size_t input_size) { +size_t WorkingMemory::RequiredSize(size_t input_size) { const size_t max_fragment_size = std::min(input_size, kBlockSize); const size_t table_size = CalculateTableSize(max_fragment_size); - size_ = table_size * sizeof(*table_) + max_fragment_size + - MaxCompressedLength(max_fragment_size); - mem_ = std::allocator<char>().allocate(size_); + return table_size * sizeof(uint16_t) + max_fragment_size + + MaxCompressedLength(max_fragment_size); +} + +WorkingMemory::WorkingMemory(size_t input_size) + : WorkingMemory(input_size, + std::allocator<char>().allocate(RequiredSize(input_size))) { + owns_mem_ = true; +} + +WorkingMemory::WorkingMemory(size_t input_size, char* buffer) { + assert(buffer != nullptr); + assert(reinterpret_cast<uintptr_t>(buffer) % alignof(uint16_t) == 0); + const size_t max_fragment_size = std::min(input_size, kBlockSize); + const size_t table_size = CalculateTableSize(max_fragment_size); + mem_ = buffer; + size_ = RequiredSize(input_size); + owns_mem_ = false; table_ = reinterpret_cast<uint16_t*>(mem_); input_ = mem_ + table_size * sizeof(*table_); output_ = input_ + max_fragment_size; } WorkingMemory::~WorkingMemory() { - std::allocator<char>().deallocate(mem_, size_); + if (owns_mem_) { + std::allocator<char>().deallocate(mem_, size_); + } } uint16_t* WorkingMemory::GetHashTable(size_t fragment_size, @@ -1953,20 +1971,51 @@ } CompressionContext::CompressionContext() - : working_memory_(new internal::WorkingMemory(kBlockSize)) {} + : working_memory_(new internal::WorkingMemory(kBlockSize)), + owns_working_memory_(true) {} -CompressionContext::~CompressionContext() { delete working_memory_; } +size_t CompressionContext::WorkspaceSize() { + return sizeof(internal::WorkingMemory) + + internal::WorkingMemory::RequiredSize(kBlockSize); +} + +CompressionContext::CompressionContext(void* workspace, size_t workspace_size) + : owns_working_memory_(false) { + assert(workspace != nullptr); + assert(workspace_size >= WorkspaceSize()); + assert(reinterpret_cast<uintptr_t>(workspace) % + alignof(internal::WorkingMemory) == + 0); + (void)workspace_size; + char* base = static_cast<char*>(workspace); + working_memory_ = new (base) internal::WorkingMemory( + kBlockSize, base + sizeof(internal::WorkingMemory)); +} + +void CompressionContext::Reset() { + if (working_memory_ == nullptr) return; + if (owns_working_memory_) { + delete working_memory_; + } else { + working_memory_->~WorkingMemory(); + } + working_memory_ = nullptr; +} + +CompressionContext::~CompressionContext() { Reset(); } CompressionContext::CompressionContext(CompressionContext&& other) noexcept - : working_memory_(other.working_memory_) { + : working_memory_(other.working_memory_), + owns_working_memory_(other.owns_working_memory_) { other.working_memory_ = nullptr; } CompressionContext& CompressionContext::operator=( CompressionContext&& other) noexcept { if (this != &other) { - delete working_memory_; + Reset(); working_memory_ = other.working_memory_; + owns_working_memory_ = other.owns_working_memory_; other.working_memory_ = nullptr; } return *this;
diff --git a/snappy.h b/snappy.h index 423142b..5413153 100644 --- a/snappy.h +++ b/snappy.h
@@ -88,7 +88,18 @@ // may only be destroyed or assigned to. class CompressionContext { public: + // Allocates the working memory on the heap. CompressionContext(); + + // Constructs a context whose working memory is placed in the + // caller-provided "workspace" instead of being heap-allocated; the + // library performs no allocation at all. + // + // REQUIRES: "workspace" points to at least "workspace_size" bytes with + // "workspace_size >= WorkspaceSize()", is suitably aligned for any + // object type (as if returned by malloc), and outlives "*this". + CompressionContext(void* workspace, size_t workspace_size); + ~CompressionContext(); CompressionContext(CompressionContext&& other) noexcept; @@ -97,11 +108,20 @@ CompressionContext(const CompressionContext&) = delete; CompressionContext& operator=(const CompressionContext&) = delete; + // The workspace size required by the non-allocating constructor above. + static size_t WorkspaceSize(); + private: friend size_t Compress(Source* reader, Sink* writer, CompressionOptions options, CompressionContext* ctx); + // Destroys the working memory as appropriate for how it was created + // (delete if heap-allocated, in-place destruction if placement-constructed + // in a caller-provided workspace). + void Reset(); + internal::WorkingMemory* working_memory_; + bool owns_working_memory_; }; // ------------------------------------------------------------------------
diff --git a/snappy_unittest.cc b/snappy_unittest.cc index 2ee6e22..4f44cbf 100644 --- a/snappy_unittest.cc +++ b/snappy_unittest.cc
@@ -595,6 +595,58 @@ } } +TEST(Snappy, CompressionContextStaticWorkspace) { + // The library performs no allocation for a context constructed over a + // caller-provided workspace. + std::vector<char> workspace(CompressionContext::WorkspaceSize()); + CompressionContext static_ctx(workspace.data(), workspace.size()); + CompressionContext heap_ctx; + + const size_t sizes[] = {0, 1, kBlockSize - 1, kBlockSize + 1, + 2 * kBlockSize + 17}; + for (size_t len : sizes) { + std::string input; + input.reserve(len); + while (input.size() < len) { + input.push_back(static_cast<char>('a' + input.size() % 7)); + } + + std::string with_static(MaxCompressedLength(len), '\0'); + size_t with_static_len = 0; + RawCompress(input.data(), input.size(), &with_static[0], &with_static_len, + CompressionOptions{}, &static_ctx); + with_static.resize(with_static_len); + + std::string with_heap(MaxCompressedLength(len), '\0'); + size_t with_heap_len = 0; + RawCompress(input.data(), input.size(), &with_heap[0], &with_heap_len, + CompressionOptions{}, &heap_ctx); + with_heap.resize(with_heap_len); + + EXPECT_EQ(with_static, with_heap) << "len=" << len; + + std::string uncompressed; + EXPECT_TRUE(Uncompress(with_static, &uncompressed)); + EXPECT_EQ(input, uncompressed); + } + + // Both context flavors keep working after being moved. + CompressionContext moved_static(std::move(static_ctx)); + CompressionContext moved_heap = std::move(heap_ctx); + const std::string input = "the quick brown fox jumps over the lazy dog"; + std::string a(MaxCompressedLength(input.size()), '\0'); + std::string b(MaxCompressedLength(input.size()), '\0'); + size_t a_len = 0; + size_t b_len = 0; + RawCompress(input.data(), input.size(), &a[0], &a_len, CompressionOptions{}, + &moved_static); + RawCompress(input.data(), input.size(), &b[0], &b_len, CompressionOptions{}, + &moved_heap); + a.resize(a_len); + b.resize(b_len); + EXPECT_EQ(a, b); +} + TEST(Snappy, FourByteOffset) { // The new compressor cannot generate four-byte offsets since // it chops up the input into 32KB pieces. So we hand-emit the