Skip to content

Commit

Permalink
compress jit debuginfo for easy memory savings (#55180)
Browse files Browse the repository at this point in the history
In some ad-hoc testing, I had JIT about 19 MB of code and data, which
generated about 170 MB of debuginfo alongside it, and that debuginfo
then compressed to about 50 MB with this change, which simply compresses
the ObjectFile until it is actually required (which it very rarely is
needed).
  • Loading branch information
vtjnash authored Jul 23, 2024
1 parent d68befd commit fe597c1
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 82 deletions.
22 changes: 15 additions & 7 deletions src/debug-registry.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,18 +99,26 @@ class JITDebugInfoRegistry
};
private:

struct ObjectInfo {
const llvm::object::ObjectFile *object = nullptr;
size_t SectionSize = 0;
ptrdiff_t slide = 0;
llvm::object::SectionRef Section{};
llvm::DIContext *context = nullptr;
struct LazyObjectInfo {
SmallVector<uint8_t, 0> data;
size_t uncompressedsize;
std::unique_ptr<const llvm::object::ObjectFile> object;
std::unique_ptr<llvm::DIContext> context;
LazyObjectInfo() = delete;
};

struct SectionInfo {
LazyObjectInfo *object;
size_t SectionSize;
ptrdiff_t slide;
uint64_t SectionIndex;
SectionInfo() = delete;
};

template<typename KeyT, typename ValT>
using rev_map = std::map<KeyT, ValT, std::greater<KeyT>>;

typedef rev_map<size_t, ObjectInfo> objectmap_t;
typedef rev_map<size_t, SectionInfo> objectmap_t;
typedef rev_map<uint64_t, objfileentry_t> objfilemap_t;

objectmap_t objectmap{};
Expand Down
57 changes: 42 additions & 15 deletions src/debuginfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <llvm/DebugInfo/DWARF/DWARFContext.h>
#include <llvm/Object/SymbolSize.h>
#include <llvm/Support/MemoryBuffer.h>
#include <llvm/Support/MemoryBufferRef.h>
#include <llvm/IR/Function.h>
#include <llvm/ADT/StringRef.h>
#include <llvm/ADT/StringMap.h>
Expand Down Expand Up @@ -335,8 +336,12 @@ void JITDebugInfoRegistry::registerJITObject(const object::ObjectFile &Object,
#endif // defined(_OS_X86_64_)
#endif // defined(_OS_WINDOWS_)

SmallVector<uint8_t, 0> packed;
compression::zlib::compress(ArrayRef<uint8_t>((uint8_t*)Object.getData().data(), Object.getData().size()), packed, compression::zlib::DefaultCompression);
jl_jit_add_bytes(packed.size());
auto ObjectCopy = new LazyObjectInfo{packed, Object.getData().size()}; // intentionally leaked so that we don't need to ref-count it, intentionally copied so that we exact-size the allocation (since no shrink_to_fit function)
auto symbols = object::computeSymbolSizes(Object);
bool first = true;
bool hassection = false;
for (const auto &sym_size : symbols) {
const object::SymbolRef &sym_iter = sym_size.first;
object::SymbolRef::Type SymbolType = cantFail(sym_iter.getType());
Expand Down Expand Up @@ -385,17 +390,17 @@ void JITDebugInfoRegistry::registerJITObject(const object::ObjectFile &Object,
jl_profile_atomic([&]() JL_NOTSAFEPOINT {
if (mi)
linfomap[Addr] = std::make_pair(Size, mi);
if (first) {
objectmap[SectionLoadAddr] = {&Object,
(size_t)SectionSize,
(ptrdiff_t)(SectionAddr - SectionLoadAddr),
*Section,
nullptr,
};
first = false;
}
hassection = true;
objectmap.insert(std::pair{SectionLoadAddr, SectionInfo{
ObjectCopy,
(size_t)SectionSize,
(ptrdiff_t)(SectionAddr - SectionLoadAddr),
Section->getIndex()
}});
});
}
if (!hassection) // clang-sa demands that we do this to fool cplusplus.NewDeleteLeaks
delete ObjectCopy;
}

void jl_register_jit_object(const object::ObjectFile &Object,
Expand Down Expand Up @@ -1213,11 +1218,33 @@ int jl_DI_for_fptr(uint64_t fptr, uint64_t *symsize, int64_t *slide,
auto fit = objmap.lower_bound(fptr);
if (fit != objmap.end() && fptr < fit->first + fit->second.SectionSize) {
*slide = fit->second.slide;
*Section = fit->second.Section;
if (context) {
if (fit->second.context == nullptr)
fit->second.context = DWARFContext::create(*fit->second.object).release();
*context = fit->second.context;
auto lazyobject = fit->second.object;
if (!lazyobject->object && !lazyobject->data.empty()) {
if (lazyobject->uncompressedsize) {
SmallVector<uint8_t, 0> unpacked;
Error E = compression::zlib::decompress(lazyobject->data, unpacked, lazyobject->uncompressedsize);
if (E)
lazyobject->data.clear();
else
lazyobject->data = std::move(unpacked);
jl_jit_add_bytes(lazyobject->data.size() - lazyobject->uncompressedsize);
lazyobject->uncompressedsize = 0;
}
if (!lazyobject->data.empty()) {
auto obj = object::ObjectFile::createObjectFile(MemoryBufferRef(StringRef((const char*)lazyobject->data.data(), lazyobject->data.size()), "jit.o"));
if (obj)
lazyobject->object = std::move(*obj);
else
lazyobject->data.clear();
}
}
if (lazyobject->object) {
*Section = *std::next(lazyobject->object->section_begin(), fit->second.SectionIndex);
if (context) {
if (lazyobject->context == nullptr)
lazyobject->context = DWARFContext::create(*lazyobject->object);
*context = lazyobject->context.get();
}
}
found = 1;
}
Expand Down
1 change: 1 addition & 0 deletions src/debuginfo.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// This file is a part of Julia. License is MIT: https://julialang.org/license

// Declarations for debuginfo.cpp
void jl_jit_add_bytes(size_t bytes) JL_NOTSAFEPOINT;

int jl_DI_for_fptr(uint64_t fptr, uint64_t *symsize, int64_t *slide,
llvm::object::SectionRef *Section, llvm::DIContext **context) JL_NOTSAFEPOINT;
Expand Down
93 changes: 34 additions & 59 deletions src/jitlayers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -684,22 +684,19 @@ struct JITObjectInfo {
class JLDebuginfoPlugin : public ObjectLinkingLayer::Plugin {
std::mutex PluginMutex;
std::map<MaterializationResponsibility *, std::unique_ptr<JITObjectInfo>> PendingObjs;
// Resources from distinct `MaterializationResponsibility`s can get merged
// after emission, so we can have multiple debug objects per resource key.
std::map<ResourceKey, SmallVector<std::unique_ptr<JITObjectInfo>, 0>> RegisteredObjs;

public:
void notifyMaterializing(MaterializationResponsibility &MR, jitlink::LinkGraph &G,
jitlink::JITLinkContext &Ctx,
MemoryBufferRef InputObject) override
{
// Keeping around a full copy of the input object file (and re-parsing it) is
// wasteful, but for now, this lets us reuse the existing debuginfo.cpp code.
// Should look into just directly pulling out all the information required in
// a JITLink pass and just keeping the required tables/DWARF sections around
// (perhaps using the LLVM DebuggerSupportPlugin as a reference).
auto NewBuffer =
MemoryBuffer::getMemBufferCopy(InputObject.getBuffer(), G.getName());
// Re-parsing the InputObject is wasteful, but for now, this lets us
// reuse the existing debuginfo.cpp code. Should look into just
// directly pulling out all the information required in a JITLink pass
// and just keeping the required tables/DWARF sections around (perhaps
// using the LLVM DebuggerSupportPlugin as a reference).
auto NewObj =
cantFail(object::ObjectFile::createObjectFile(NewBuffer->getMemBufferRef()));

Expand Down Expand Up @@ -733,13 +730,8 @@ class JLDebuginfoPlugin : public ObjectLinkingLayer::Plugin {
};

jl_register_jit_object(*NewInfo->Object, getLoadAddress, nullptr);
}

cantFail(MR.withResourceKeyDo([&](ResourceKey K) {
std::lock_guard<std::mutex> lock(PluginMutex);
RegisteredObjs[K].push_back(std::move(PendingObjs[&MR]));
PendingObjs.erase(&MR);
}));
}

return Error::success();
}
Expand All @@ -750,32 +742,23 @@ class JLDebuginfoPlugin : public ObjectLinkingLayer::Plugin {
PendingObjs.erase(&MR);
return Error::success();
}

#if JL_LLVM_VERSION >= 160000
Error notifyRemovingResources(JITDylib &JD, orc::ResourceKey K) override
#else
Error notifyRemovingResources(ResourceKey K) override
Error notifyRemovingResources(orc::ResourceKey K) override
#endif
{
std::lock_guard<std::mutex> lock(PluginMutex);
RegisteredObjs.erase(K);
// TODO: If we ever unload code, need to notify debuginfo registry.
return Error::success();
}

#if JL_LLVM_VERSION >= 160000
void notifyTransferringResources(JITDylib &JD, ResourceKey DstKey, ResourceKey SrcKey) override
void notifyTransferringResources(JITDylib &JD, orc::ResourceKey DstKey,
orc::ResourceKey SrcKey) override {}
#else
void notifyTransferringResources(ResourceKey DstKey, ResourceKey SrcKey) override
void notifyTransferringResources(orc::ResourceKey DstKey,
orc::ResourceKey SrcKey) override {}
#endif
{
std::lock_guard<std::mutex> lock(PluginMutex);
auto SrcIt = RegisteredObjs.find(SrcKey);
if (SrcIt != RegisteredObjs.end()) {
for (std::unique_ptr<JITObjectInfo> &Info : SrcIt->second)
RegisteredObjs[DstKey].push_back(std::move(Info));
RegisteredObjs.erase(SrcIt);
}
}

void modifyPassConfig(MaterializationResponsibility &MR, jitlink::LinkGraph &,
jitlink::PassConfiguration &PassConfig) override
Expand Down Expand Up @@ -815,12 +798,12 @@ class JLDebuginfoPlugin : public ObjectLinkingLayer::Plugin {

class JLMemoryUsagePlugin : public ObjectLinkingLayer::Plugin {
private:
std::atomic<size_t> &total_size;
std::atomic<size_t> &jit_bytes_size;

public:

JLMemoryUsagePlugin(std::atomic<size_t> &total_size)
: total_size(total_size) {}
JLMemoryUsagePlugin(std::atomic<size_t> &jit_bytes_size)
: jit_bytes_size(jit_bytes_size) {}

Error notifyFailed(orc::MaterializationResponsibility &MR) override {
return Error::success();
Expand Down Expand Up @@ -869,7 +852,7 @@ class JLMemoryUsagePlugin : public ObjectLinkingLayer::Plugin {
}
(void) code_size;
(void) data_size;
this->total_size.fetch_add(graph_size, std::memory_order_relaxed);
this->jit_bytes_size.fetch_add(graph_size, std::memory_order_relaxed);
jl_timing_counter_inc(JL_TIMING_COUNTER_JITSize, graph_size);
jl_timing_counter_inc(JL_TIMING_COUNTER_JITCodeSize, code_size);
jl_timing_counter_inc(JL_TIMING_COUNTER_JITDataSize, data_size);
Expand Down Expand Up @@ -985,24 +968,7 @@ void registerRTDyldJITObject(const object::ObjectFile &Object,
const RuntimeDyld::LoadedObjectInfo &L,
const std::shared_ptr<RTDyldMemoryManager> &MemMgr)
{
auto SavedObject = L.getObjectForDebug(Object).takeBinary();
// If the debug object is unavailable, save (a copy of) the original object
// for our backtraces.
// This copy seems unfortunate, but there doesn't seem to be a way to take
// ownership of the original buffer.
if (!SavedObject.first) {
auto NewBuffer =
MemoryBuffer::getMemBufferCopy(Object.getData(), Object.getFileName());
auto NewObj =
cantFail(object::ObjectFile::createObjectFile(NewBuffer->getMemBufferRef()));
SavedObject = std::make_pair(std::move(NewObj), std::move(NewBuffer));
}
const object::ObjectFile *DebugObj = SavedObject.first.release();
SavedObject.second.release();

StringMap<object::SectionRef> loadedSections;
// Use the original Object, not the DebugObject, as this is used for the
// RuntimeDyld::LoadedObjectInfo lookup.
for (const object::SectionRef &lSection : Object.sections()) {
auto sName = lSection.getName();
if (sName) {
Expand All @@ -1019,7 +985,9 @@ void registerRTDyldJITObject(const object::ObjectFile &Object,
return L.getSectionLoadAddress(search->second);
};

jl_register_jit_object(*DebugObj, getLoadAddress,
auto DebugObject = L.getObjectForDebug(Object); // ELF requires us to make a copy to mutate the header with the section load addresses. On other platforms this is a no-op.
jl_register_jit_object(DebugObject.getBinary() ? *DebugObject.getBinary() : Object,
getLoadAddress,
#if defined(_OS_WINDOWS_) && defined(_CPU_X86_64_)
[MemMgr](void *p) { return lookupWriteAddressFor(MemMgr.get(), p); }
#else
Expand Down Expand Up @@ -1630,7 +1598,7 @@ JuliaOJIT::JuliaOJIT()
ES, std::move(ehRegistrar)));

ObjectLayer.addPlugin(std::make_unique<JLDebuginfoPlugin>());
ObjectLayer.addPlugin(std::make_unique<JLMemoryUsagePlugin>(total_size));
ObjectLayer.addPlugin(std::make_unique<JLMemoryUsagePlugin>(jit_bytes_size));
#else
ObjectLayer.setNotifyLoaded(
[this](orc::MaterializationResponsibility &MR,
Expand Down Expand Up @@ -2042,19 +2010,20 @@ std::string JuliaOJIT::getMangledName(const GlobalValue *GV)
return getMangledName(GV->getName());
}

#ifdef JL_USE_JITLINK
size_t JuliaOJIT::getTotalBytes() const
{
return total_size.load(std::memory_order_relaxed);
auto bytes = jit_bytes_size.load(std::memory_order_relaxed);
#ifndef JL_USE_JITLINK
size_t getRTDyldMemoryManagerTotalBytes(RTDyldMemoryManager *mm) JL_NOTSAFEPOINT;
bytes += getRTDyldMemoryManagerTotalBytes(MemMgr.get());
#endif
return bytes;
}
#else
size_t getRTDyldMemoryManagerTotalBytes(RTDyldMemoryManager *mm) JL_NOTSAFEPOINT;

size_t JuliaOJIT::getTotalBytes() const
void JuliaOJIT::addBytes(size_t bytes)
{
return getRTDyldMemoryManagerTotalBytes(MemMgr.get());
jit_bytes_size.fetch_add(bytes, std::memory_order_relaxed);
}
#endif

void JuliaOJIT::printTimers()
{
Expand Down Expand Up @@ -2339,3 +2308,9 @@ size_t jl_jit_total_bytes_impl(void)
{
return jl_ExecutionEngine->getTotalBytes();
}

// API for adding bytes to record being owned by the JIT
void jl_jit_add_bytes(size_t bytes)
{
jl_ExecutionEngine->addBytes(bytes);
}
3 changes: 2 additions & 1 deletion src/jitlayers.h
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ class JuliaOJIT {
TargetIRAnalysis getTargetIRAnalysis() const JL_NOTSAFEPOINT;

size_t getTotalBytes() const JL_NOTSAFEPOINT;
void addBytes(size_t bytes) JL_NOTSAFEPOINT;
void printTimers() JL_NOTSAFEPOINT;

jl_locked_stream &get_dump_emitted_mi_name_stream() JL_NOTSAFEPOINT {
Expand Down Expand Up @@ -605,10 +606,10 @@ class JuliaOJIT {

ResourcePool<orc::ThreadSafeContext, 0, std::queue<orc::ThreadSafeContext>> ContextPool;

std::atomic<size_t> jit_bytes_size{0};
#ifndef JL_USE_JITLINK
const std::shared_ptr<RTDyldMemoryManager> MemMgr;
#else
std::atomic<size_t> total_size{0};
const std::unique_ptr<jitlink::JITLinkMemoryManager> MemMgr;
#endif
ObjLayerT ObjectLayer;
Expand Down

0 comments on commit fe597c1

Please sign in to comment.