mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 21:30:12 +01:00
Make file_test dramatically faster. (#3834)
Our testing had started to take more noticable time so I looked at where the time went to see if it could be improved easily. Almost all of the time was spent building regex matchers. That code path allocates a lot of memory and does a lot of processing in order to make the regex fast to run over the input. While that's not really a great tradeoff for how we use these matchers, the bigger issue is that we don't need a regex is the vast majority of cases. This change inspects the pattern more deeply to avoid most of the work. First, it looks for the cases where the pattern doesn't require any adjustment at all and directly builds a matcher from that if it can. This avoids an extra string allocation entirely. This is probably only a minor improvement, but it is also very easy. The larger change is to process the string in two phases. First, we expand the keywords and check for a regex region. If we find no regex region, we can directly build a string equality matcher for the expanded string. This still allocates an extra copy of the string but is *dramatically* cheaper that building a regex. Finally, if we *do* find a regex, we re-process the string to escape everything and transform the regex sequence into its valid form before building the matcher. The result for me is an over 5x reduction in test time. Profiling afterward shows a lot more opportunities for optimization here if things get slow again. The actual toolchain code isn't really visible in the profile yet. Note, I was profiling the normal build, which has asserts and ASan and such. I've not looked at the optimized build.
This commit is contained in:
@@ -472,21 +472,15 @@ static auto TryConsumeSplit(
|
||||
return true;
|
||||
}
|
||||
|
||||
// Transforms an expectation on a given line from `FileCheck` syntax into a
|
||||
// standard regex matcher.
|
||||
static auto TransformExpectation(int line_index, llvm::StringRef in)
|
||||
-> ErrorOr<Matcher<std::string>> {
|
||||
if (in.empty()) {
|
||||
return Matcher<std::string>{StrEq("")};
|
||||
}
|
||||
if (in[0] != ' ') {
|
||||
return ErrorBuilder() << "Malformated CHECK line: " << in;
|
||||
}
|
||||
std::string str = in.substr(1).str();
|
||||
// Converts a `FileCheck`-style expectation string into a single complete regex
|
||||
// string by escaping all regex characters outside of the designated `{{...}}`
|
||||
// regex sequences, and switching those to a normal regex sub-pattern syntax.
|
||||
static void ConvertExpectationStringToRegex(std::string& str) {
|
||||
for (int pos = 0; pos < static_cast<int>(str.size());) {
|
||||
switch (str[pos]) {
|
||||
case '(':
|
||||
case ')':
|
||||
case '[':
|
||||
case ']':
|
||||
case '}':
|
||||
case '.':
|
||||
@@ -502,51 +496,21 @@ static auto TransformExpectation(int line_index, llvm::StringRef in)
|
||||
pos += 2;
|
||||
break;
|
||||
}
|
||||
case '[': {
|
||||
llvm::StringRef line_keyword_cursor = llvm::StringRef(str).substr(pos);
|
||||
if (line_keyword_cursor.consume_front("[[")) {
|
||||
static constexpr llvm::StringLiteral LineKeyword = "@LINE";
|
||||
if (line_keyword_cursor.consume_front(LineKeyword)) {
|
||||
// Allow + or - here; consumeInteger handles -.
|
||||
line_keyword_cursor.consume_front("+");
|
||||
int offset;
|
||||
// consumeInteger returns true for errors, not false.
|
||||
if (line_keyword_cursor.consumeInteger(10, offset) ||
|
||||
!line_keyword_cursor.consume_front("]]")) {
|
||||
return ErrorBuilder()
|
||||
<< "Unexpected @LINE offset at `"
|
||||
<< line_keyword_cursor.substr(0, 5) << "` in: " << in;
|
||||
}
|
||||
std::string int_str = llvm::Twine(line_index + offset).str();
|
||||
int remove_len = (line_keyword_cursor.data() - str.data()) - pos;
|
||||
str.replace(pos, remove_len, int_str);
|
||||
pos += int_str.size();
|
||||
} else {
|
||||
return ErrorBuilder()
|
||||
<< "Unexpected [[, should be {{\\[\\[}} at `"
|
||||
<< line_keyword_cursor.substr(0, 5) << "` in: " << in;
|
||||
}
|
||||
} else {
|
||||
// Escape the `[`.
|
||||
str.insert(pos, "\\");
|
||||
pos += 2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '{': {
|
||||
if (pos + 1 == static_cast<int>(str.size()) || str[pos + 1] != '{') {
|
||||
// Single `{`, escape it.
|
||||
str.insert(pos, "\\");
|
||||
pos += 2;
|
||||
} else {
|
||||
// Replace the `{{...}}` regex syntax with standard `(...)` syntax.
|
||||
str.replace(pos, 2, "(");
|
||||
for (++pos; pos < static_cast<int>(str.size() - 1); ++pos) {
|
||||
if (str[pos] == '}' && str[pos + 1] == '}') {
|
||||
str.replace(pos, 2, ")");
|
||||
++pos;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Replace the `{{...}}` regex syntax with standard `(...)` syntax.
|
||||
str.replace(pos, 2, "(");
|
||||
for (++pos; pos < static_cast<int>(str.size() - 1); ++pos) {
|
||||
if (str[pos] == '}' && str[pos + 1] == '}') {
|
||||
str.replace(pos, 2, ")");
|
||||
++pos;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -556,7 +520,75 @@ static auto TransformExpectation(int line_index, llvm::StringRef in)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transforms an expectation on a given line from `FileCheck` syntax into a
|
||||
// standard regex matcher.
|
||||
static auto TransformExpectation(int line_index, llvm::StringRef in)
|
||||
-> ErrorOr<Matcher<std::string>> {
|
||||
if (in.empty()) {
|
||||
return Matcher<std::string>{StrEq("")};
|
||||
}
|
||||
if (!in.consume_front(" ")) {
|
||||
return ErrorBuilder() << "Malformated CHECK line: " << in;
|
||||
}
|
||||
|
||||
// Check early if we have a regex component as we can avoid building an
|
||||
// expensive matcher when not using those.
|
||||
bool has_regex = in.find("{{") != llvm::StringRef::npos;
|
||||
|
||||
// Now scan the string and expand any keywords. Note that this needs to be
|
||||
// `size_t` to correctly store `npos`.
|
||||
size_t keyword_pos = in.find("[[");
|
||||
|
||||
// If there are neither keywords nor regex sequences, we can match the
|
||||
// incoming string directly.
|
||||
if (!has_regex && keyword_pos == llvm::StringRef::npos) {
|
||||
return Matcher<std::string>{StrEq(in)};
|
||||
}
|
||||
|
||||
std::string str = in.str();
|
||||
|
||||
// First expand the keywords.
|
||||
while (keyword_pos != std::string::npos) {
|
||||
llvm::StringRef line_keyword_cursor =
|
||||
llvm::StringRef(str).substr(keyword_pos);
|
||||
CARBON_CHECK(line_keyword_cursor.consume_front("[["));
|
||||
|
||||
static constexpr llvm::StringLiteral LineKeyword = "@LINE";
|
||||
if (!line_keyword_cursor.consume_front(LineKeyword)) {
|
||||
return ErrorBuilder()
|
||||
<< "Unexpected [[, should be {{\\[\\[}} at `"
|
||||
<< line_keyword_cursor.substr(0, 5) << "` in: " << in;
|
||||
}
|
||||
|
||||
// Allow + or - here; consumeInteger handles -.
|
||||
line_keyword_cursor.consume_front("+");
|
||||
int offset;
|
||||
// consumeInteger returns true for errors, not false.
|
||||
if (line_keyword_cursor.consumeInteger(10, offset) ||
|
||||
!line_keyword_cursor.consume_front("]]")) {
|
||||
return ErrorBuilder()
|
||||
<< "Unexpected @LINE offset at `"
|
||||
<< line_keyword_cursor.substr(0, 5) << "` in: " << in;
|
||||
}
|
||||
std::string int_str = llvm::Twine(line_index + offset).str();
|
||||
int remove_len = (line_keyword_cursor.data() - str.data()) - keyword_pos;
|
||||
str.replace(keyword_pos, remove_len, int_str);
|
||||
keyword_pos += int_str.size();
|
||||
// Find the next keyword start or the end of the string.
|
||||
keyword_pos = str.find("[[", keyword_pos);
|
||||
}
|
||||
|
||||
// If there was no regex, we can directly match the adjusted string.
|
||||
if (!has_regex) {
|
||||
return Matcher<std::string>{StrEq(str)};
|
||||
}
|
||||
|
||||
// Otherwise, we need to turn the entire string into a regex by escaping
|
||||
// things outside the regex region and transforming the regex region into a
|
||||
// normal syntax.
|
||||
ConvertExpectationStringToRegex(str);
|
||||
return Matcher<std::string>{MatchesRegex(str)};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user