Improve Json::Value memory allocation for strings:

The memory allocation patterns of Json::Value benefit greatly
from the slabbed allocator. This commit adds a global slabbed
allocator dedicated to `Json::Value`.

Real-world data indicates that only 2% of allocation requests
are over 72 bytes long. The remaining 98% of allocations fall
into the following 3 buckets, calculated across 9,500,000,000
allocation calls:

    [ 1, 32]: 17% of all allocations
    [33, 48]: 27% of all allocations
    [49, 72]: 57% of all allocations

This commit should result in improved performance for servers
that have JSON-heavy workloads, typically those servicing RPC
and WebSocket workloads, and less memory fragmentation.
This commit is contained in:
Nik Bougalis
2023-08-30 21:12:46 -07:00
parent 3637cc19f0
commit 086ce21c0d
5 changed files with 123 additions and 65 deletions

View File

@@ -19,7 +19,9 @@
#include <ripple/app/main/Application.h>
#include <ripple/app/main/DBInit.h>
#include <ripple/app/rdb/Vacuum.h>
#include <ripple/basics/ByteUtilities.h>
#include <ripple/basics/Log.h>
#include <ripple/basics/SlabAllocator.h>
#include <ripple/basics/StringUtilities.h>
#include <ripple/basics/contract.h>
#include <ripple/beast/clock/basic_seconds_clock.h>
@@ -27,6 +29,7 @@
#include <ripple/core/Config.h>
#include <ripple/core/ConfigSections.h>
#include <ripple/core/TimeKeeper.h>
#include <ripple/json/json_value.h>
#include <ripple/json/to_string.h>
#include <ripple/net/RPCCall.h>
#include <ripple/protocol/BuildInfo.h>
@@ -344,6 +347,56 @@ runUnitTests(
#endif // ENABLE_TESTS
//------------------------------------------------------------------------------
class SlabbedValueAllocator : public Json::ValueAllocator
{
struct Buffer
{
alignas(8) char buffer_[32];
};
SlabAllocatorSet<Buffer> slabber_;
public:
SlabbedValueAllocator()
: slabber_({
// clang-format off
{ 0, 2 * 1024 * 1024 },
{16, 6 * 1024 * 1024 },
{40, 9 * 1024 * 1024 }
// clang-format on
})
{
}
virtual ~SlabbedValueAllocator() = default;
char*
duplicateStringValue(const char* value, std::size_t length = 0) override
{
if (length == 0 && value != nullptr)
length = std::strlen(value);
if (auto ret = reinterpret_cast<char*>(slabber_.allocate(length + 1)))
{
std::copy_n(value, length, ret);
ret[length] = 0;
return ret;
}
// Fall back to the default if we can't grab memory from the slab
// allocators.
return ValueAllocator::duplicateStringValue(value, length);
}
void
releaseStringValue(char* value) override
{
if (!slabber_.deallocate(reinterpret_cast<std::uint8_t*>(value)))
ValueAllocator::releaseStringValue(value);
}
};
//------------------------------------------------------------------------------
int
run(int argc, char** argv)
{
@@ -520,6 +573,9 @@ run(int argc, char** argv)
return 0;
}
// Do this early enough so that it's useful for unit tests too:
Json::setAllocator(new SlabbedValueAllocator());
#ifndef ENABLE_TESTS
if (vm.count("unittest") || vm.count("unittest-child"))
{

View File

@@ -27,7 +27,8 @@
#include <ripple/protocol/STLedgerEntry.h>
#include <cstdint>
#include <optional>
#include <memory>
#include <type_traits>
namespace ripple {

View File

@@ -20,6 +20,7 @@
#ifndef RIPPLE_BASICS_SLABALLOCATOR_H_INCLUDED
#define RIPPLE_BASICS_SLABALLOCATOR_H_INCLUDED
#include <ripple/basics/ByteUtilities.h>
#include <ripple/beast/type_name.h>
#include <algorithm>
#include <atomic>

View File

@@ -23,6 +23,12 @@
#include <ripple/json/json_writer.h>
#include <ripple/json/to_string.h>
#include <atomic>
#include <iostream>
#include <ripple/basics/ByteUtilities.h>
#include <ripple/basics/SlabAllocator.h>
namespace Json {
const Value Value::null;
@@ -30,64 +36,45 @@ const Int Value::minInt = Int(~(UInt(-1) / 2));
const Int Value::maxInt = Int(UInt(-1) / 2);
const UInt Value::maxUInt = UInt(-1);
class DefaultValueAllocator : public ValueAllocator
char*
ValueAllocator::duplicateStringValue(const char* value, std::size_t length)
{
public:
virtual ~DefaultValueAllocator() = default;
//@todo investigate this old optimization
// if ( !value || value[0] == 0 )
// return 0;
char*
makeMemberName(const char* memberName) override
{
return duplicateStringValue(memberName);
}
if (length == 0 && value != nullptr)
length = strlen(value);
void
releaseMemberName(char* memberName) override
{
releaseStringValue(memberName);
}
auto ret = new char[length + 1];
char*
duplicateStringValue(const char* value, unsigned int length = unknown)
override
{
//@todo investigate this old optimization
// if ( !value || value[0] == 0 )
// return 0;
if (value != nullptr && length != 0)
memcpy(ret, value, length);
if (length == unknown)
length = value ? (unsigned int)strlen(value) : 0;
char* newString = static_cast<char*>(malloc(length + 1));
if (value)
memcpy(newString, value, length);
newString[length] = 0;
return newString;
}
void
releaseStringValue(char* value) override
{
if (value)
free(value);
}
};
static ValueAllocator*&
valueAllocator()
{
static ValueAllocator* valueAllocator = new DefaultValueAllocator;
return valueAllocator;
ret[length] = 0;
return ret;
}
static struct DummyValueAllocatorInitializer
void
ValueAllocator::releaseStringValue(char* value)
{
DummyValueAllocatorInitializer()
{
valueAllocator(); // ensure valueAllocator() statics are initialized
// before main().
}
} dummyValueAllocatorInitializer;
delete[] value;
}
/** The default allocator to use, if no custom allocator is specified. */
static ValueAllocator defaultValueAllocator;
/** A pointer to the allocator to use. */
static constinit ValueAllocator* valueAllocator = &defaultValueAllocator;
void setAllocator(ValueAllocator *allocator)
{
assert(allocator != nullptr && valueAllocator == &defaultValueAllocator);
if (valueAllocator == &defaultValueAllocator)
valueAllocator = allocator;
}
// //////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////
@@ -106,7 +93,7 @@ Value::CZString::CZString(int index) : cstr_(0), index_(index)
Value::CZString::CZString(const char* cstr, DuplicationPolicy allocate)
: cstr_(
allocate == duplicate ? valueAllocator()->makeMemberName(cstr) : cstr)
allocate == duplicate ? valueAllocator->makeMemberName(cstr) : cstr)
, index_(allocate)
{
}
@@ -114,7 +101,7 @@ Value::CZString::CZString(const char* cstr, DuplicationPolicy allocate)
Value::CZString::CZString(const CZString& other)
: cstr_(
other.index_ != noDuplication && other.cstr_ != 0
? valueAllocator()->makeMemberName(other.cstr_)
? valueAllocator->makeMemberName(other.cstr_)
: other.cstr_)
, index_(
other.cstr_
@@ -126,7 +113,7 @@ Value::CZString::CZString(const CZString& other)
Value::CZString::~CZString()
{
if (cstr_ && index_ == duplicate)
valueAllocator()->releaseMemberName(const_cast<char*>(cstr_));
valueAllocator->releaseMemberName(const_cast<char*>(cstr_));
}
bool
@@ -228,7 +215,7 @@ Value::Value(double value) : type_(realValue)
Value::Value(const char* value) : type_(stringValue), allocated_(true)
{
value_.string_ = valueAllocator()->duplicateStringValue(value);
value_.string_ = valueAllocator->duplicateStringValue(value);
}
Value::Value(std::string_view value) : type_(stringValue), allocated_(true)
@@ -239,8 +226,8 @@ Value::Value(std::string_view value) : type_(stringValue), allocated_(true)
Value::Value(std::string const& value) : type_(stringValue), allocated_(true)
{
value_.string_ = valueAllocator()->duplicateStringValue(
value.c_str(), (unsigned int)value.length());
value_.string_ = valueAllocator->duplicateStringValue(
value.c_str(), value.length());
}
Value::Value(const StaticString& value) : type_(stringValue), allocated_(false)
@@ -268,7 +255,7 @@ Value::Value(const Value& other) : type_(other.type_)
case stringValue:
if (other.value_.string_)
{
value_.string_ = valueAllocator()->duplicateStringValue(
value_.string_ = valueAllocator->duplicateStringValue(
other.value_.string_);
allocated_ = true;
}
@@ -300,7 +287,7 @@ Value::~Value()
case stringValue:
if (allocated_)
valueAllocator()->releaseStringValue(value_.string_);
valueAllocator->releaseStringValue(value_.string_);
break;

View File

@@ -480,20 +480,33 @@ operator>=(const Value& x, const Value& y)
class ValueAllocator
{
public:
enum { unknown = (unsigned)-1 };
virtual ~ValueAllocator() = default;
virtual char*
makeMemberName(const char* memberName) = 0;
makeMemberName(const char* memberName)
{
return duplicateStringValue(memberName);
}
virtual void
releaseMemberName(char* memberName) = 0;
releaseMemberName(char* memberName)
{
releaseStringValue(memberName);
}
virtual char*
duplicateStringValue(const char* value, unsigned int length = unknown) = 0;
duplicateStringValue(const char* value, std::size_t length = 0);
virtual void
releaseStringValue(char* value) = 0;
releaseStringValue(char* value);
};
/** Assigns an allocator to use for string-related JSON memory requests.
@note This can only be called once, and should be called early.
*/
void setAllocator(ValueAllocator *allocator);
/** \brief base class for Value iterators.
*
*/