Skip to content

Commit 79971d0

Browse files
committed
[llvm-profdata] Add option to cap profile output size
D139603 (add option to llvm-profdata to reduce output profile size) contains test cases that are not cross-platform. Moving those tests to unit test and making sure the feature is callable from llvm library Reviewed By: snehasish Differential Revision: https://reviews.llvm.org/D141446
1 parent 829bcb0 commit 79971d0

File tree

7 files changed

+430
-26
lines changed

7 files changed

+430
-26
lines changed

llvm/include/llvm/ProfileData/SampleProf.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,14 @@ class SampleRecord {
427427
void print(raw_ostream &OS, unsigned Indent) const;
428428
void dump() const;
429429

430+
bool operator==(const SampleRecord &Other) const {
431+
return NumSamples == Other.NumSamples && CallTargets == Other.CallTargets;
432+
}
433+
434+
bool operator!=(const SampleRecord &Other) const {
435+
return !(*this == Other);
436+
}
437+
430438
private:
431439
uint64_t NumSamples = 0;
432440
CallTargetMap CallTargets;
@@ -1149,6 +1157,21 @@ class FunctionSamples {
11491157
// all the inline instances and names of call targets.
11501158
void findAllNames(DenseSet<StringRef> &NameSet) const;
11511159

1160+
bool operator==(const FunctionSamples &Other) const {
1161+
return (GUIDToFuncNameMap == Other.GUIDToFuncNameMap ||
1162+
(GUIDToFuncNameMap && Other.GUIDToFuncNameMap &&
1163+
*GUIDToFuncNameMap == *Other.GUIDToFuncNameMap)) &&
1164+
FunctionHash == Other.FunctionHash && Context == Other.Context &&
1165+
TotalSamples == Other.TotalSamples &&
1166+
TotalHeadSamples == Other.TotalHeadSamples &&
1167+
BodySamples == Other.BodySamples &&
1168+
CallsiteSamples == Other.CallsiteSamples;
1169+
}
1170+
1171+
bool operator!=(const FunctionSamples &Other) const {
1172+
return !(*this == Other);
1173+
}
1174+
11521175
private:
11531176
/// CFG hash value for the function.
11541177
uint64_t FunctionHash = 0;

llvm/include/llvm/ProfileData/SampleProfWriter.h

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,56 @@ enum SectionLayout {
3535
NumOfLayout,
3636
};
3737

38+
/// When writing a profile with size limit, user may want to use a different
39+
/// strategy to reduce function count other than dropping functions with fewest
40+
/// samples first. In this case a class implementing the same interfaces should
41+
/// be provided to SampleProfileWriter::writeWithSizeLimit().
42+
class FunctionPruningStrategy {
43+
protected:
44+
SampleProfileMap &ProfileMap;
45+
size_t OutputSizeLimit;
46+
47+
public:
48+
/// \p ProfileMap A reference to the original profile map. It will be modified
49+
/// by Erase().
50+
/// \p OutputSizeLimit Size limit in bytes of the output profile. This is
51+
/// necessary to estimate how many functions to remove.
52+
FunctionPruningStrategy(SampleProfileMap &ProfileMap, size_t OutputSizeLimit)
53+
: ProfileMap(ProfileMap), OutputSizeLimit(OutputSizeLimit) {}
54+
55+
virtual ~FunctionPruningStrategy() = default;
56+
57+
/// SampleProfileWriter::writeWithSizeLimit() calls this after every write
58+
/// iteration if the output size still exceeds the limit. This function
59+
/// should erase some functions from the profile map so that the writer tries
60+
/// to write the profile again with fewer functions. At least 1 entry from the
61+
/// profile map must be erased.
62+
///
63+
/// \p CurrentOutputSize Number of bytes in the output if current profile map
64+
/// is written.
65+
virtual void Erase(size_t CurrentOutputSize) = 0;
66+
};
67+
68+
class DefaultFunctionPruningStrategy : public FunctionPruningStrategy {
69+
std::vector<NameFunctionSamples> SortedFunctions;
70+
71+
public:
72+
DefaultFunctionPruningStrategy(SampleProfileMap &ProfileMap,
73+
size_t OutputSizeLimit);
74+
75+
/// In this default implementation, functions with fewest samples are dropped
76+
/// first. Since the exact size of the output cannot be easily calculated due
77+
/// to compression, we use a heuristic to remove as many functions as
78+
/// necessary but not too many, aiming to minimize the number of write
79+
/// iterations.
80+
/// Empirically, functions with larger total sample count contain linearly
81+
/// more sample entries, meaning it takes linearly more space to write them.
82+
/// The cumulative length is therefore quadratic if all functions are sorted
83+
/// by total sample count.
84+
/// TODO: Find better heuristic.
85+
void Erase(size_t CurrentOutputSize) override;
86+
};
87+
3888
/// Sample-based profile writer. Base class.
3989
class SampleProfileWriter {
4090
public:
@@ -50,6 +100,17 @@ class SampleProfileWriter {
50100
/// \returns status code of the file update operation.
51101
virtual std::error_code write(const SampleProfileMap &ProfileMap);
52102

103+
/// Write sample profiles up to given size limit, using the pruning strategy
104+
/// to drop some functions if necessary.
105+
///
106+
/// \returns status code of the file update operation.
107+
template <typename FunctionPruningStrategy = DefaultFunctionPruningStrategy>
108+
std::error_code writeWithSizeLimit(SampleProfileMap &ProfileMap,
109+
size_t OutputSizeLimit) {
110+
FunctionPruningStrategy Strategy(ProfileMap, OutputSizeLimit);
111+
return writeWithSizeLimitInternal(ProfileMap, OutputSizeLimit, &Strategy);
112+
}
113+
53114
raw_ostream &getOutputStream() { return *OutputStream; }
54115

55116
/// Profile writer factory.
@@ -79,6 +140,15 @@ class SampleProfileWriter {
79140
// Write function profiles to the profile file.
80141
virtual std::error_code writeFuncProfiles(const SampleProfileMap &ProfileMap);
81142

143+
std::error_code writeWithSizeLimitInternal(SampleProfileMap &ProfileMap,
144+
size_t OutputSizeLimit,
145+
FunctionPruningStrategy *Strategy);
146+
147+
/// For writeWithSizeLimit in text mode, each newline takes 1 additional byte
148+
/// on Windows when actually written to the file, but not written to a memory
149+
/// buffer. This needs to be accounted for when rewriting the profile.
150+
size_t LineCount;
151+
82152
/// Output stream where to emit the profile to.
83153
std::unique_ptr<raw_ostream> OutputStream;
84154

@@ -102,6 +172,7 @@ class SampleProfileWriterText : public SampleProfileWriter {
102172
: SampleProfileWriter(OS), Indent(0) {}
103173

104174
std::error_code writeHeader(const SampleProfileMap &ProfileMap) override {
175+
LineCount = 0;
105176
return sampleprof_error::success;
106177
}
107178

llvm/lib/ProfileData/SampleProfWriter.cpp

Lines changed: 129 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,117 @@
3030
#include "llvm/Support/MD5.h"
3131
#include "llvm/Support/raw_ostream.h"
3232
#include <algorithm>
33+
#include <cmath>
3334
#include <cstdint>
3435
#include <memory>
3536
#include <set>
3637
#include <system_error>
3738
#include <utility>
3839
#include <vector>
3940

41+
#define DEBUG_TYPE "llvm-profdata"
42+
4043
using namespace llvm;
4144
using namespace sampleprof;
4245

46+
namespace llvm {
47+
namespace support {
48+
namespace endian {
49+
namespace {
50+
51+
// Adapter class to llvm::support::endian::Writer for pwrite().
52+
struct SeekableWriter {
53+
raw_pwrite_stream &OS;
54+
endianness Endian;
55+
SeekableWriter(raw_pwrite_stream &OS, endianness Endian)
56+
: OS(OS), Endian(Endian) {}
57+
58+
template <typename ValueType>
59+
void pwrite(ValueType Val, size_t Offset) {
60+
std::string StringBuf;
61+
raw_string_ostream SStream(StringBuf);
62+
Writer(SStream, Endian).write(Val);
63+
OS.pwrite(StringBuf.data(), StringBuf.size(), Offset);
64+
}
65+
};
66+
67+
} // namespace
68+
} // namespace endian
69+
} // namespace support
70+
} // namespace llvm
71+
72+
DefaultFunctionPruningStrategy::DefaultFunctionPruningStrategy(
73+
SampleProfileMap &ProfileMap, size_t OutputSizeLimit)
74+
: FunctionPruningStrategy(ProfileMap, OutputSizeLimit) {
75+
sortFuncProfiles(ProfileMap, SortedFunctions);
76+
}
77+
78+
void DefaultFunctionPruningStrategy::Erase(size_t CurrentOutputSize) {
79+
double D = (double)OutputSizeLimit / CurrentOutputSize;
80+
size_t NewSize = (size_t)round(ProfileMap.size() * D * D);
81+
size_t NumToRemove = ProfileMap.size() - NewSize;
82+
if (NumToRemove < 1)
83+
NumToRemove = 1;
84+
85+
assert(NumToRemove <= SortedFunctions.size());
86+
llvm::for_each(
87+
llvm::make_range(SortedFunctions.begin() + SortedFunctions.size() -
88+
NumToRemove,
89+
SortedFunctions.end()),
90+
[&](const NameFunctionSamples &E) { ProfileMap.erase(E.first); });
91+
SortedFunctions.resize(SortedFunctions.size() - NumToRemove);
92+
}
93+
94+
std::error_code SampleProfileWriter::writeWithSizeLimitInternal(
95+
SampleProfileMap &ProfileMap, size_t OutputSizeLimit,
96+
FunctionPruningStrategy *Strategy) {
97+
if (OutputSizeLimit == 0)
98+
return write(ProfileMap);
99+
100+
size_t OriginalFunctionCount = ProfileMap.size();
101+
102+
std::unique_ptr<raw_ostream> OriginalOutputStream;
103+
OutputStream.swap(OriginalOutputStream);
104+
105+
size_t IterationCount = 0;
106+
size_t TotalSize;
107+
108+
SmallVector<char> StringBuffer;
109+
do {
110+
StringBuffer.clear();
111+
OutputStream.reset(new raw_svector_ostream(StringBuffer));
112+
if (std::error_code EC = write(ProfileMap))
113+
return EC;
114+
115+
TotalSize = StringBuffer.size();
116+
// On Windows every "\n" is actually written as "\r\n" to disk but not to
117+
// memory buffer, this difference should be added when considering the total
118+
// output size.
119+
#ifdef _WIN32
120+
if (Format == SPF_Text)
121+
TotalSize += LineCount;
122+
#endif
123+
if (TotalSize <= OutputSizeLimit)
124+
break;
125+
126+
Strategy->Erase(TotalSize);
127+
IterationCount++;
128+
} while (ProfileMap.size() != 0);
129+
130+
if (ProfileMap.size() == 0)
131+
return sampleprof_error::too_large;
132+
133+
OutputStream.swap(OriginalOutputStream);
134+
OutputStream->write(StringBuffer.data(), StringBuffer.size());
135+
LLVM_DEBUG(dbgs() << "Profile originally has " << OriginalFunctionCount
136+
<< " functions, reduced to " << ProfileMap.size() << " in "
137+
<< IterationCount << " iterations\n");
138+
// Silence warning on Release build.
139+
(void)OriginalFunctionCount;
140+
(void)IterationCount;
141+
return sampleprof_error::success;
142+
}
143+
43144
std::error_code
44145
SampleProfileWriter::writeFuncProfiles(const SampleProfileMap &ProfileMap) {
45146
std::vector<NameFunctionSamples> V;
@@ -116,6 +217,12 @@ std::error_code SampleProfileWriterExtBinaryBase::addNewSection(
116217

117218
std::error_code
118219
SampleProfileWriterExtBinaryBase::write(const SampleProfileMap &ProfileMap) {
220+
// When calling write on a different profile map, existing states should be
221+
// cleared.
222+
NameTable.clear();
223+
CSNameTable.clear();
224+
SecHdrTable.clear();
225+
119226
if (std::error_code EC = writeHeader(ProfileMap))
120227
return EC;
121228

@@ -477,6 +584,7 @@ std::error_code SampleProfileWriterText::writeSample(const FunctionSamples &S) {
477584
if (Indent == 0)
478585
OS << ":" << S.getHeadSamples();
479586
OS << "\n";
587+
LineCount++;
480588

481589
SampleSorter<LineLocation, SampleRecord> SortedSamples(S.getBodySamples());
482590
for (const auto &I : SortedSamples.get()) {
@@ -493,6 +601,7 @@ std::error_code SampleProfileWriterText::writeSample(const FunctionSamples &S) {
493601
for (const auto &J : Sample.getSortedCallTargets())
494602
OS << " " << J.first << ":" << J.second;
495603
OS << "\n";
604+
LineCount++;
496605
}
497606

498607
SampleSorter<LineLocation, FunctionSamplesMap> SortedCallsiteSamples(
@@ -515,11 +624,13 @@ std::error_code SampleProfileWriterText::writeSample(const FunctionSamples &S) {
515624
if (FunctionSamples::ProfileIsProbeBased) {
516625
OS.indent(Indent + 1);
517626
OS << "!CFGChecksum: " << S.getFunctionHash() << "\n";
627+
LineCount++;
518628
}
519629

520630
if (S.getContext().getAllAttributes()) {
521631
OS.indent(Indent + 1);
522632
OS << "!Attributes: " << S.getContext().getAllAttributes() << "\n";
633+
LineCount++;
523634
}
524635

525636
return sampleprof_error::success;
@@ -605,14 +716,10 @@ std::error_code SampleProfileWriterCompactBinary::writeFuncOffsetTable() {
605716
auto &OS = *OutputStream;
606717

607718
// Fill the slot remembered by TableOffset with the offset of FuncOffsetTable.
608-
auto &OFS = static_cast<raw_fd_ostream &>(OS);
609719
uint64_t FuncOffsetTableStart = OS.tell();
610-
if (OFS.seek(TableOffset) == (uint64_t)-1)
611-
return sampleprof_error::ostream_seek_unsupported;
612-
support::endian::Writer Writer(*OutputStream, support::little);
613-
Writer.write(FuncOffsetTableStart);
614-
if (OFS.seek(FuncOffsetTableStart) == (uint64_t)-1)
615-
return sampleprof_error::ostream_seek_unsupported;
720+
support::endian::SeekableWriter Writer(static_cast<raw_pwrite_stream &>(OS),
721+
support::little);
722+
Writer.pwrite(FuncOffsetTableStart, TableOffset);
616723

617724
// Write out the table size.
618725
encodeULEB128(FuncOffsetTable.size(), OS);
@@ -623,6 +730,7 @@ std::error_code SampleProfileWriterCompactBinary::writeFuncOffsetTable() {
623730
return EC;
624731
encodeULEB128(Entry.second, OS);
625732
}
733+
FuncOffsetTable.clear();
626734
return sampleprof_error::success;
627735
}
628736

@@ -650,6 +758,10 @@ SampleProfileWriterBinary::writeMagicIdent(SampleProfileFormat Format) {
650758

651759
std::error_code
652760
SampleProfileWriterBinary::writeHeader(const SampleProfileMap &ProfileMap) {
761+
// When calling write on a different profile map, existing names should be
762+
// cleared.
763+
NameTable.clear();
764+
653765
writeMagicIdent(Format);
654766

655767
computeSummary(ProfileMap);
@@ -690,14 +802,6 @@ void SampleProfileWriterExtBinaryBase::allocSecHdrTable() {
690802
}
691803

692804
std::error_code SampleProfileWriterExtBinaryBase::writeSecHdrTable() {
693-
auto &OFS = static_cast<raw_fd_ostream &>(*OutputStream);
694-
uint64_t Saved = OutputStream->tell();
695-
696-
// Set OutputStream to the location saved in SecHdrTableOffset.
697-
if (OFS.seek(SecHdrTableOffset) == (uint64_t)-1)
698-
return sampleprof_error::ostream_seek_unsupported;
699-
support::endian::Writer Writer(*OutputStream, support::little);
700-
701805
assert(SecHdrTable.size() == SectionHdrLayout.size() &&
702806
"SecHdrTable entries doesn't match SectionHdrLayout");
703807
SmallVector<uint32_t, 16> IndexMap(SecHdrTable.size(), -1);
@@ -714,21 +818,23 @@ std::error_code SampleProfileWriterExtBinaryBase::writeSecHdrTable() {
714818
// needs to be computed after SecLBRProfile (the order in SecHdrTable),
715819
// but it needs to be read before SecLBRProfile (the order in
716820
// SectionHdrLayout). So we use IndexMap above to switch the order.
821+
support::endian::SeekableWriter Writer(
822+
static_cast<raw_pwrite_stream &>(*OutputStream), support::little);
717823
for (uint32_t LayoutIdx = 0; LayoutIdx < SectionHdrLayout.size();
718824
LayoutIdx++) {
719825
assert(IndexMap[LayoutIdx] < SecHdrTable.size() &&
720826
"Incorrect LayoutIdx in SecHdrTable");
721827
auto Entry = SecHdrTable[IndexMap[LayoutIdx]];
722-
Writer.write(static_cast<uint64_t>(Entry.Type));
723-
Writer.write(static_cast<uint64_t>(Entry.Flags));
724-
Writer.write(static_cast<uint64_t>(Entry.Offset));
725-
Writer.write(static_cast<uint64_t>(Entry.Size));
828+
Writer.pwrite(static_cast<uint64_t>(Entry.Type),
829+
SecHdrTableOffset + 4 * LayoutIdx * sizeof(uint64_t));
830+
Writer.pwrite(static_cast<uint64_t>(Entry.Flags),
831+
SecHdrTableOffset + (4 * LayoutIdx + 1) * sizeof(uint64_t));
832+
Writer.pwrite(static_cast<uint64_t>(Entry.Offset),
833+
SecHdrTableOffset + (4 * LayoutIdx + 2) * sizeof(uint64_t));
834+
Writer.pwrite(static_cast<uint64_t>(Entry.Size),
835+
SecHdrTableOffset + (4 * LayoutIdx + 3) * sizeof(uint64_t));
726836
}
727837

728-
// Reset OutputStream.
729-
if (OFS.seek(Saved) == (uint64_t)-1)
730-
return sampleprof_error::ostream_seek_unsupported;
731-
732838
return sampleprof_error::success;
733839
}
734840

0 commit comments

Comments
 (0)