Skip to content

Conversation

dtcxzyw
Copy link
Member

@dtcxzyw dtcxzyw commented Oct 2, 2025

Closes #161636.

Compile-time impact (+0.06%): https://llvm-compile-time-tracker.com/compare.php?from=c2ef022aa7413ddc9aba48fa6fbe6fbd0cb14e19&to=9a0f0302efc30580136d191e66bac929f08ee25f&stat=instructions%3Au
I used to disable this fold for pointers, because I cannot construct a positive test that is covered by foldSelectValueEquivalence but not covered by simplifySelectWithICmpCond. But the IR diff shows we still benefit from the fold in InstCombine:

@dtcxzyw dtcxzyw requested a review from nikic as a code owner October 2, 2025 17:09
@llvmbot llvmbot added llvm:instcombine Covers the InstCombine, InstSimplify and AggressiveInstCombine passes llvm:transforms labels Oct 2, 2025
@llvmbot
Copy link
Member

llvmbot commented Oct 2, 2025

@llvm/pr-subscribers-llvm-transforms

Author: Yingwei Zheng (dtcxzyw)

Changes

Closes #161636.

Compile-time impact (+0.02%): https://llvm-compile-time-tracker.com/compare.php?from=17f6888d1771c9f61378a0a58725f3359277ddda&to=44cdcb84b1a9d996cd61f2c37ac4c9df5bc531af&stat=instructions%3Au
I used to disable this fold for pointers, because I cannot construct a positive test that is covered by foldSelectValueEquivalence but not covered by simplifySelectWithICmpCond. But the IR diff shows we still benefit from the fold in InstCombine:

Perhaps we can cache the query result of getUnderlyingObjectAggressive to improve the compile time.


Full diff: https://github.com/llvm/llvm-project/pull/161701.diff

2 Files Affected:

  • (modified) llvm/lib/Transforms/InstCombine/InstCombineSelect.cpp (+11-6)
  • (modified) llvm/test/Transforms/InstCombine/select-gep.ll (+32)
diff --git a/llvm/lib/Transforms/InstCombine/InstCombineSelect.cpp b/llvm/lib/Transforms/InstCombine/InstCombineSelect.cpp
index 87000a1c36eef..5ea79b92633d9 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineSelect.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineSelect.cpp
@@ -17,6 +17,7 @@
 #include "llvm/Analysis/AssumptionCache.h"
 #include "llvm/Analysis/CmpInstAnalysis.h"
 #include "llvm/Analysis/InstructionSimplify.h"
+#include "llvm/Analysis/Loads.h"
 #include "llvm/Analysis/OverflowInstAnalysis.h"
 #include "llvm/Analysis/ValueTracking.h"
 #include "llvm/Analysis/VectorUtils.h"
@@ -1411,6 +1412,8 @@ Instruction *InstCombinerImpl::foldSelectValueEquivalence(SelectInst &Sel,
     // in the cmp and in f(Y).
     if (TrueVal == OldOp && (isa<Constant>(OldOp) || !isa<Constant>(NewOp)))
       return nullptr;
+    if (!canReplacePointersIfEqual(OldOp, NewOp, DL))
+      return nullptr;
 
     if (Value *V = simplifyWithOpReplaced(TrueVal, OldOp, NewOp, SQ,
                                           /* AllowRefinement=*/true)) {
@@ -1466,12 +1469,14 @@ Instruction *InstCombinerImpl::foldSelectValueEquivalence(SelectInst &Sel,
   // Example:
   // (X == 42) ? 43 : (X + 1) --> (X == 42) ? (X + 1) : (X + 1) --> X + 1
   SmallVector<Instruction *> DropFlags;
-  if (simplifyWithOpReplaced(FalseVal, CmpLHS, CmpRHS, SQ,
-                             /* AllowRefinement */ false,
-                             &DropFlags) == TrueVal ||
-      simplifyWithOpReplaced(FalseVal, CmpRHS, CmpLHS, SQ,
-                             /* AllowRefinement */ false,
-                             &DropFlags) == TrueVal) {
+  if ((canReplacePointersIfEqual(CmpLHS, CmpRHS, DL) &&
+       simplifyWithOpReplaced(FalseVal, CmpLHS, CmpRHS, SQ,
+                              /* AllowRefinement */ false,
+                              &DropFlags) == TrueVal) ||
+      (canReplacePointersIfEqual(CmpRHS, CmpLHS, DL) &&
+       simplifyWithOpReplaced(FalseVal, CmpRHS, CmpLHS, SQ,
+                              /* AllowRefinement */ false,
+                              &DropFlags) == TrueVal)) {
     for (Instruction *I : DropFlags) {
       I->dropPoisonGeneratingAnnotations();
       Worklist.add(I);
diff --git a/llvm/test/Transforms/InstCombine/select-gep.ll b/llvm/test/Transforms/InstCombine/select-gep.ll
index dd8dffba11b05..718133699a8a7 100644
--- a/llvm/test/Transforms/InstCombine/select-gep.ll
+++ b/llvm/test/Transforms/InstCombine/select-gep.ll
@@ -286,3 +286,35 @@ define <2 x ptr> @test7(<2 x ptr> %p1, i64 %idx, <2 x i1> %cc) {
   %select = select <2 x i1> %cc, <2 x ptr> %p1, <2 x ptr> %gep
   ret <2 x ptr> %select
 }
+
+define ptr @ptr_eq_replace_freeze1(ptr %p, ptr %q) {
+; CHECK-LABEL: @ptr_eq_replace_freeze1(
+; CHECK-NEXT:    [[Q_FR:%.*]] = freeze ptr [[Q:%.*]]
+; CHECK-NEXT:    [[Q_FR1:%.*]] = freeze ptr [[Q1:%.*]]
+; CHECK-NEXT:    [[CMP:%.*]] = icmp eq ptr [[Q_FR]], [[Q_FR1]]
+; CHECK-NEXT:    [[SELECT:%.*]] = select i1 [[CMP]], ptr [[Q_FR]], ptr [[Q_FR1]]
+; CHECK-NEXT:    ret ptr [[SELECT]]
+;
+  %p.fr = freeze ptr %p
+  %q.fr = freeze ptr %q
+  %cmp = icmp eq ptr %p.fr, %q.fr
+  %select = select i1 %cmp, ptr %p.fr, ptr %q.fr
+  ret ptr %select
+}
+
+define ptr @ptr_eq_replace_freeze2(ptr %p, ptr %q) {
+; CHECK-LABEL: @ptr_eq_replace_freeze2(
+; CHECK-NEXT:    [[P_FR:%.*]] = freeze ptr [[P:%.*]]
+; CHECK-NEXT:    [[P_FR1:%.*]] = freeze ptr [[P1:%.*]]
+; CHECK-NEXT:    [[CMP:%.*]] = icmp eq ptr [[P_FR1]], [[P_FR]]
+; CHECK-NEXT:    [[SELECT_V:%.*]] = select i1 [[CMP]], ptr [[P_FR1]], ptr [[P_FR]]
+; CHECK-NEXT:    [[SELECT:%.*]] = getelementptr i8, ptr [[SELECT_V]], i64 16
+; CHECK-NEXT:    ret ptr [[SELECT]]
+;
+  %gep1 = getelementptr i32, ptr %p, i64 4
+  %gep2 = getelementptr i32, ptr %q, i64 4
+  %cmp = icmp eq ptr %p, %q
+  %cmp.fr = freeze i1 %cmp
+  %select = select i1 %cmp.fr, ptr %gep1, ptr %gep2
+  ret ptr %select
+}

@antoniofrighetto
Copy link
Contributor

Maybe I'm missing something, but at least from the tests, aren't the three canReplacePointersIfEqual being invoked on the same pointers all the times? Couldn't this be done only once?

@dtcxzyw
Copy link
Member Author

dtcxzyw commented Oct 2, 2025

Maybe I'm missing something, but at least from the tests, aren't the three canReplacePointersIfEqual being invoked on the same pointers all the times? Couldn't this be done only once?

It is not commutative. You can always replace a pointer with a known dereferencable one.

@antoniofrighetto
Copy link
Contributor

Maybe I'm missing something, but at least from the tests, aren't the three canReplacePointersIfEqual being invoked on the same pointers all the times? Couldn't this be done only once?

It is not commutative. You can always replace a pointer with a known dereferencable one.

Sorry, I'm not clear. I do understand isDereferenceablePointer is called on different pointers, though, now, canReplacePointersIfEqual(CmpLHS, CmpRHS) is being called twice (one in ReplaceOldOpWithNewOp, one later), and so is canReplacePointersIfEqual(CmpRHS, CmpLHS). Why cannot this be done once for both the operands?

@dtcxzyw
Copy link
Member Author

dtcxzyw commented Oct 2, 2025

Maybe I'm missing something, but at least from the tests, aren't the three canReplacePointersIfEqual being invoked on the same pointers all the times? Couldn't this be done only once?

It is not commutative. You can always replace a pointer with a known dereferencable one.

Sorry, I'm not clear. I do understand isDereferenceablePointer is called on different pointers, though, now, canReplacePointersIfEqual(CmpLHS, CmpRHS) is being called twice (one in ReplaceOldOpWithNewOp, one later), and so is canReplacePointersIfEqual(CmpRHS, CmpLHS). Why cannot this be done once for both the operands?

I will cache the result.

@antoniofrighetto
Copy link
Contributor

Maybe I'm missing something, but at least from the tests, aren't the three canReplacePointersIfEqual being invoked on the same pointers all the times? Couldn't this be done only once?

It is not commutative. You can always replace a pointer with a known dereferencable one.

Sorry, I'm not clear. I do understand isDereferenceablePointer is called on different pointers, though, now, canReplacePointersIfEqual(CmpLHS, CmpRHS) is being called twice (one in ReplaceOldOpWithNewOp, one later), and so is canReplacePointersIfEqual(CmpRHS, CmpLHS). Why cannot this be done once for both the operands?

I will cache the result.

Thanks. BTW, what I had in mind originally was something along the following lines:

bool RP1 = canReplacePointersIfEqual(CmpLHS, CmpRHS, DL);
if (Instruction *R = ReplaceOldOpWithNewOp(CmpLHS, CmpRHS, RP1))
  return R;

bool RP2 = canReplacePointersIfEqual(CmpRHS, CmpLHS, DL);
if (Instruction *R = ReplaceOldOpWithNewOp(CmpRHS, CmpLHS, RP2))
  return R;

And later if ((RP1 && simplifyWithOpReplaced()) || (RP2 && simplifyWithOpReplaced()).

; CHECK-NEXT: ret ptr [[SELECT]]
;
%p.fr = freeze ptr %p
%q.fr = freeze ptr %q
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the significance of freeze here? Why does this issue not reproduce without it (or with just noundef)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is guarded by

auto *FalseInst = dyn_cast<Instruction>(FalseVal);
if (!FalseInst)
return nullptr;

@nikic
Copy link
Contributor

nikic commented Oct 2, 2025

I don't think we really need the caching. There doesn't seem to be any significant compile-time impact, so I'd rather not duplicate this logic.

auto ReplaceOldOpWithNewOp = [&](Value *OldOp,
Value *NewOp) -> Instruction * {
auto ReplaceOldOpWithNewOp = [&](Value *OldOp, Value *NewOp,
uint32_t Direction) -> Instruction * {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused Direction argument?

Copy link
Contributor

@nikic nikic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@dtcxzyw dtcxzyw merged commit c41611b into llvm:main Oct 5, 2025
9 checks passed
@dtcxzyw dtcxzyw deleted the perf/fix-161636 branch October 5, 2025 06:02
@llvm-ci
Copy link
Collaborator

llvm-ci commented Oct 5, 2025

LLVM Buildbot has detected a new failure on builder sanitizer-aarch64-linux running on sanitizer-buildbot8 while building llvm at step 2 "annotate".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/51/builds/24687

Here is the relevant piece of the build log for the reference
Step 2 (annotate) failure: 'python ../sanitizer_buildbot/sanitizers/zorg/buildbot/builders/sanitizers/buildbot_selector.py' (failure) (timed out)
...
[181/186] Generating MSAN_INST_GTEST.gtest-all.cc.aarch64.o
[182/186] Generating MSAN_INST_TEST_OBJECTS.msan_test.cpp.aarch64-with-call.o
[183/186] Generating Msan-aarch64-with-call-Test
[184/186] Generating MSAN_INST_TEST_OBJECTS.msan_test.cpp.aarch64.o
[185/186] Generating Msan-aarch64-Test
[185/186] Running compiler_rt regression tests
llvm-lit: /home/b/sanitizer-aarch64-linux/build/llvm-project/llvm/utils/lit/lit/discovery.py:276: warning: input '/home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/test/interception/Unit' contained no tests
llvm-lit: /home/b/sanitizer-aarch64-linux/build/llvm-project/llvm/utils/lit/lit/discovery.py:276: warning: input '/home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/test/sanitizer_common/Unit' contained no tests
llvm-lit: /home/b/sanitizer-aarch64-linux/build/llvm-project/llvm/utils/lit/lit/main.py:74: note: The test suite configuration requested an individual test timeout of 0 seconds but a timeout of 900 seconds was requested on the command line. Forcing timeout to be 900 seconds.
-- Testing: 5981 tests, 72 workers --
command timed out: 1200 seconds without output running [b'python', b'../sanitizer_buildbot/sanitizers/zorg/buildbot/builders/sanitizers/buildbot_selector.py'], attempting to kill
process killed by signal 9
program finished with exit code -1
elapsedTime=1555.745156
Testing:  0.. 10.. 20.. 30.. 40.. 50.. 60.. 70.. 80.. 90..
Step 9 (test compiler-rt symbolizer) failure: test compiler-rt symbolizer (failure)
...
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std/type_traits.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std/typeindex.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std/typeinfo.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std/unordered_map.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std/unordered_set.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std/utility.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std/valarray.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std/variant.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std/vector.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std/version.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/cassert.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/cctype.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/cerrno.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/cfenv.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/cfloat.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/cinttypes.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/climits.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/clocale.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/cmath.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/csetjmp.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/csignal.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/cstdarg.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/cstddef.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/cstdint.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/cstdio.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/cstdlib.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/cstring.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/ctime.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/cuchar.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/cwchar.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat/cwctype.inc
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.cppm
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/share/libc++/v1/std.compat.cppm
-- Installing: /home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/lib/tsan/libcxx_tsan_aarch64/lib/libc++.modules.json
[181/186] Generating MSAN_INST_GTEST.gtest-all.cc.aarch64.o
[182/186] Generating MSAN_INST_TEST_OBJECTS.msan_test.cpp.aarch64-with-call.o
[183/186] Generating Msan-aarch64-with-call-Test
[184/186] Generating MSAN_INST_TEST_OBJECTS.msan_test.cpp.aarch64.o
[185/186] Generating Msan-aarch64-Test
[185/186] Running compiler_rt regression tests
llvm-lit: /home/b/sanitizer-aarch64-linux/build/llvm-project/llvm/utils/lit/lit/discovery.py:276: warning: input '/home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/test/interception/Unit' contained no tests
llvm-lit: /home/b/sanitizer-aarch64-linux/build/llvm-project/llvm/utils/lit/lit/discovery.py:276: warning: input '/home/b/sanitizer-aarch64-linux/build/build_default/runtimes/runtimes-bins/compiler-rt/test/sanitizer_common/Unit' contained no tests
llvm-lit: /home/b/sanitizer-aarch64-linux/build/llvm-project/llvm/utils/lit/lit/main.py:74: note: The test suite configuration requested an individual test timeout of 0 seconds but a timeout of 900 seconds was requested on the command line. Forcing timeout to be 900 seconds.
-- Testing: 5981 tests, 72 workers --

command timed out: 1200 seconds without output running [b'python', b'../sanitizer_buildbot/sanitizers/zorg/buildbot/builders/sanitizers/buildbot_selector.py'], attempting to kill
process killed by signal 9
program finished with exit code -1
elapsedTime=1555.745156
Testing:  0.. 10.. 20.. 30.. 40.. 50.. 60.. 70.. 80.. 90..

aokblast pushed a commit to aokblast/llvm-project that referenced this pull request Oct 6, 2025
…llvm#161701)

Closes llvm#161636.

Compile-time impact (+0.06%):
https://llvm-compile-time-tracker.com/compare.php?from=c2ef022aa7413ddc9aba48fa6fbe6fbd0cb14e19&to=9a0f0302efc30580136d191e66bac929f08ee25f&stat=instructions%3Au
I used to disable this fold for pointers, because I cannot construct a
positive test that is covered by `foldSelectValueEquivalence ` but not
covered by `simplifySelectWithICmpCond`. But the IR diff shows we still
benefit from the fold in InstCombine:
+ Bail out on pointers:
dtcxzyw/llvm-opt-benchmark#2880
+ This patch: dtcxzyw/llvm-opt-benchmark#2882
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
llvm:instcombine Covers the InstCombine, InstSimplify and AggressiveInstCombine passes llvm:transforms
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[InstCombine] Pointer equality propagated without regard to provenance
5 participants