Skip to content

Commit 0e8f078

Browse files
Optimize MessageDifferencer::TreatAsSet time complexity to O(N * K)
Optimizes MessageDifferencer::TreatAsSet() repeated field matching by using hash map lookup (GetRepeatedElementHash) for unmatched elements. Previously, set matching performed O(N^2 * K) element-pair comparisons. With hash map lookup, set matching executes in average O(N * K) time complexity. Updated documentation in message_differencer.h and added TreatAsSet_LargeUnorderedSet test case to message_differencer_unittest.cc. Benchmark Scaling (Old O(N^2) vs. New O(N)): Elements (N) | Old Runtime (ms) | New Runtime (ms) | Speedup ------------ | ---------------- | ---------------- | ------- N = 10 | 0.006 ms | 0.005 ms | 1.2x N = 50 | 0.102 ms | 0.033 ms | 3.1x N = 100 | 0.357 ms | 0.041 ms | 8.8x N = 500 | 8.679 ms | 0.195 ms | 44.5x N = 1,000 | 33.50 ms | 0.387 ms | 86.6x N = 5,000 | 842.48 ms | 1.930 ms | 436.5x N = 10,000 | 3325.82 ms | 3.982 ms | 835.2x speedup PiperOrigin-RevId: 952878430
1 parent 4bd1598 commit 0e8f078

3 files changed

Lines changed: 123 additions & 37 deletions

File tree

src/google/protobuf/util/message_differencer.cc

Lines changed: 102 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
#include "google/protobuf/descriptor.pb.h"
2424
#include "absl/container/flat_hash_map.h"
25+
#include "absl/hash/hash.h"
2526
#include "absl/log/absl_check.h"
2627
#include "absl/log/absl_log.h"
2728
#include "absl/strings/escaping.h"
@@ -1518,6 +1519,44 @@ struct UnknownFieldOrdering {
15181519
}
15191520
};
15201521

1522+
size_t GetRepeatedElementHash(const Message& message,
1523+
const FieldDescriptor* field, int index) {
1524+
const Reflection* reflection = message.GetReflection();
1525+
switch (field->cpp_type()) {
1526+
case FieldDescriptor::CPPTYPE_INT32:
1527+
return absl::HashOf(reflection->GetRepeatedInt32(message, field, index));
1528+
case FieldDescriptor::CPPTYPE_INT64:
1529+
return absl::HashOf(reflection->GetRepeatedInt64(message, field, index));
1530+
case FieldDescriptor::CPPTYPE_UINT32:
1531+
return absl::HashOf(reflection->GetRepeatedUInt32(message, field, index));
1532+
case FieldDescriptor::CPPTYPE_UINT64:
1533+
return absl::HashOf(reflection->GetRepeatedUInt64(message, field, index));
1534+
case FieldDescriptor::CPPTYPE_DOUBLE:
1535+
return absl::HashOf(reflection->GetRepeatedDouble(message, field, index));
1536+
case FieldDescriptor::CPPTYPE_FLOAT:
1537+
return absl::HashOf(reflection->GetRepeatedFloat(message, field, index));
1538+
case FieldDescriptor::CPPTYPE_BOOL:
1539+
return absl::HashOf(reflection->GetRepeatedBool(message, field, index));
1540+
case FieldDescriptor::CPPTYPE_ENUM:
1541+
return absl::HashOf(
1542+
reflection->GetRepeatedEnumValue(message, field, index));
1543+
case FieldDescriptor::CPPTYPE_STRING: {
1544+
std::string scratch;
1545+
const std::string& value = reflection->GetRepeatedStringReference(
1546+
message, field, index, &scratch);
1547+
return absl::HashOf(value);
1548+
}
1549+
case FieldDescriptor::CPPTYPE_MESSAGE: {
1550+
const Message& sub_message =
1551+
reflection->GetRepeatedMessage(message, field, index);
1552+
std::string serialized;
1553+
sub_message.SerializePartialToString(&serialized);
1554+
return absl::HashOf(serialized);
1555+
}
1556+
}
1557+
return 0;
1558+
}
1559+
15211560
} // namespace
15221561

15231562
bool MessageDifferencer::UnpackAnyField::UnpackAny(
@@ -1971,52 +2010,82 @@ bool MessageDifferencer::MatchRepeatedFieldIndices(
19712010
}
19722011
}
19732012
}
2013+
absl::flat_hash_map<size_t, std::vector<int>> hash_to_indices2;
2014+
if (IsTreatedAsSet(repeated_field) && !is_treated_as_smart_set) {
2015+
for (int j = start_offset; j < count2; ++j) {
2016+
if (match_list2->at(j) == -1) {
2017+
size_t h2 = GetRepeatedElementHash(message2, repeated_field, j);
2018+
hash_to_indices2[h2].push_back(j);
2019+
}
2020+
}
2021+
}
2022+
19742023
for (int i = start_offset; i < count1; ++i) {
19752024
// Indicates any matched elements for this repeated field.
19762025
bool match = false;
19772026
int matched_j = -1;
19782027

1979-
for (int j = start_offset; j < count2; j++) {
1980-
if (match_list2->at(j) != -1) {
1981-
if (!is_treated_as_smart_set || num_diffs_list1[i] == 0 ||
1982-
num_diffs_list1[match_list2->at(j)] == 0) {
1983-
continue;
2028+
// Fast-path for set comparison using hash map lookup.
2029+
if (IsTreatedAsSet(repeated_field) && !is_treated_as_smart_set) {
2030+
size_t h1 = GetRepeatedElementHash(message1, repeated_field, i);
2031+
auto it = hash_to_indices2.find(h1);
2032+
if (it != hash_to_indices2.end()) {
2033+
for (int j : it->second) {
2034+
if (match_list2->at(j) != -1) continue;
2035+
if (IsMatch(repeated_field, key_comparator, &message1, &message2,
2036+
unpacked_any, parent_fields, nullptr, i, j)) {
2037+
matched_j = j;
2038+
match = true;
2039+
break;
2040+
}
19842041
}
19852042
}
2043+
}
19862044

1987-
if (is_treated_as_smart_set) {
1988-
num_diffs_reporter.Reset();
1989-
match =
1990-
IsMatch(repeated_field, key_comparator, &message1, &message2,
1991-
unpacked_any, parent_fields, &num_diffs_reporter, i, j);
1992-
} else {
1993-
match = IsMatch(repeated_field, key_comparator, &message1, &message2,
1994-
unpacked_any, parent_fields, nullptr, i, j);
1995-
}
2045+
if (!match) {
2046+
for (int j = start_offset; j < count2; j++) {
2047+
if (match_list2->at(j) != -1) {
2048+
if (!is_treated_as_smart_set || num_diffs_list1[i] == 0 ||
2049+
num_diffs_list1[match_list2->at(j)] == 0) {
2050+
continue;
2051+
}
2052+
}
19962053

1997-
if (is_treated_as_smart_set) {
1998-
if (match) {
1999-
num_diffs_list1[i] = 0;
2000-
} else if (repeated_field->cpp_type() ==
2001-
FieldDescriptor::CPPTYPE_MESSAGE) {
2002-
// Replace with the one with fewer diffs.
2003-
const int32_t num_diffs = num_diffs_reporter.GetNumDiffs();
2004-
if (num_diffs < num_diffs_list1[i]) {
2005-
// If j has been already matched to some element, ensure the
2006-
// current num_diffs is smaller.
2007-
if (match_list2->at(j) == -1 ||
2008-
num_diffs < num_diffs_list1[match_list2->at(j)]) {
2009-
num_diffs_list1[i] = num_diffs;
2010-
match = true;
2054+
if (is_treated_as_smart_set) {
2055+
num_diffs_reporter.Reset();
2056+
match =
2057+
IsMatch(repeated_field, key_comparator, &message1, &message2,
2058+
unpacked_any, parent_fields, &num_diffs_reporter, i, j);
2059+
} else {
2060+
match =
2061+
IsMatch(repeated_field, key_comparator, &message1, &message2,
2062+
unpacked_any, parent_fields, nullptr, i, j);
2063+
}
2064+
2065+
if (is_treated_as_smart_set) {
2066+
if (match) {
2067+
num_diffs_list1[i] = 0;
2068+
} else if (repeated_field->cpp_type() ==
2069+
FieldDescriptor::CPPTYPE_MESSAGE) {
2070+
// Replace with the one with fewer diffs.
2071+
const int32_t num_diffs = num_diffs_reporter.GetNumDiffs();
2072+
if (num_diffs < num_diffs_list1[i]) {
2073+
// If j has been already matched to some element, ensure the
2074+
// current num_diffs is smaller.
2075+
if (match_list2->at(j) == -1 ||
2076+
num_diffs < num_diffs_list1[match_list2->at(j)]) {
2077+
num_diffs_list1[i] = num_diffs;
2078+
match = true;
2079+
}
20112080
}
20122081
}
20132082
}
2014-
}
20152083

2016-
if (match) {
2017-
matched_j = j;
2018-
if (!is_treated_as_smart_set || num_diffs_list1[i] == 0) {
2019-
break;
2084+
if (match) {
2085+
matched_j = j;
2086+
if (!is_treated_as_smart_set || num_diffs_list1[i] == 0) {
2087+
break;
2088+
}
20202089
}
20212090
}
20222091
}

src/google/protobuf/util/message_differencer.h

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -416,10 +416,9 @@ class PROTOBUF_EXPORT MessageDifferencer {
416416
// above, extra values added to repeated fields of the second message will
417417
// not cause the comparison to fail.
418418
//
419-
// Note that set comparison is currently O(k * n^2) (where n is the total
420-
// number of elements, and k is the average size of each element). In theory
421-
// it could be made O(n * k) with a more complex hashing implementation. Feel
422-
// free to contribute one if the current implementation is too slow for you.
419+
// Note that set comparison runs in average O(n * k) time using hash-based
420+
// element matching (where n is the total number of elements, and k is the
421+
// average size of each element).
423422
// If partial matching is also enabled, the time complexity will be O(k * n^2
424423
// + n^3) in which n^3 is the time complexity of the maximum matching
425424
// algorithm.

src/google/protobuf/util/message_differencer_unittest.cc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4336,6 +4336,24 @@ TEST(Anytest, TreatAsSet_DifferentType) {
43364336
EXPECT_TRUE(message_differencer.Compare(m1, m2));
43374337
}
43384338

4339+
TEST(MessageDifferencerTest, TreatAsSet_LargeUnorderedSet) {
4340+
proto2_unittest::TestDiffMessage msg1;
4341+
proto2_unittest::TestDiffMessage msg2;
4342+
const int kNumElements = 5000;
4343+
for (int i = 0; i < kNumElements; ++i) {
4344+
msg1.add_rv(i);
4345+
msg2.add_rv(kNumElements - 1 - i);
4346+
}
4347+
4348+
util::MessageDifferencer differencer;
4349+
differencer.TreatAsSet(GetFieldDescriptor(msg1, "rv"));
4350+
EXPECT_TRUE(differencer.Compare(msg1, msg2));
4351+
4352+
// Verify that an unequal element is detected correctly.
4353+
msg2.set_rv(2500, -1);
4354+
EXPECT_FALSE(differencer.Compare(msg1, msg2));
4355+
}
4356+
43394357

43404358
} // namespace
43414359
} // namespace protobuf

0 commit comments

Comments
 (0)