diff --git a/clang/include/clang/Analysis/FlowSensitive/MatchSwitch.h b/clang/include/clang/Analysis/FlowSensitive/MatchSwitch.h
--- a/clang/include/clang/Analysis/FlowSensitive/MatchSwitch.h
+++ b/clang/include/clang/Analysis/FlowSensitive/MatchSwitch.h
@@ -25,7 +25,9 @@
 #include "clang/ASTMatchers/ASTMatchFinder.h"
 #include "clang/ASTMatchers/ASTMatchers.h"
 #include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
+#include "llvm/ADT/DenseMap.h"
 #include "llvm/ADT/StringRef.h"
+#include "llvm/Support/ErrorHandling.h"
 #include <functional>
 #include <string>
 #include <utility>
@@ -49,6 +51,16 @@
 template <typename State>
 using MatchSwitch = std::function<void(const Stmt &, ASTContext &, State &)>;
 
+/// Options that determine the behavior of a `MatchSwitchBuilder` instance.
+struct MatchSwitchBuildOptions {
+  /// If true, caches the action of a match for a given statement. As a result,
+  /// matchers will be invoked at most once for each unique statement.
+  ///
+  /// Note: This assumes that all invocations of `MatchSwitch` for a given
+  /// `Stmt` use the same `ASTContext`.
+  bool CacheActions = true;
+};
+
 /// Collects cases of a "match switch": a collection of matchers paired with
 /// callbacks, which together define a switch that can be applied to a
 /// `Stmt`. This structure can simplify the definition of `transfer` functions
@@ -91,26 +103,25 @@
     return std::move(*this);
   }
 
-  MatchSwitch<State> Build() && {
-    return [Matcher = BuildMatcher(), Actions = std::move(Actions)](
-               const Stmt &Stmt, ASTContext &Context, State &S) {
-      auto Results = ast_matchers::matchDynamic(Matcher, Stmt, Context);
-      if (Results.empty())
-        return;
-      // Look through the map for the first binding of the form "TagN..." use
-      // that to select the action.
-      for (const auto &Element : Results[0].getMap()) {
-        llvm::StringRef ID(Element.first);
-        size_t Index = 0;
-        if (ID.consume_front("Tag") && !ID.getAsInteger(10, Index) &&
-            Index < Actions.size()) {
-          Actions[Index](
-              &Stmt,
-              ast_matchers::MatchFinder::MatchResult(Results[0], &Context), S);
-          return;
-        }
-      }
-    };
+  MatchSwitch<State> Build(MatchSwitchBuildOptions Options = {}) && {
+    if (!Options.CacheActions) {
+      return [Matcher = BuildMatcher(), Actions = std::move(Actions)](
+                 const Stmt &Stmt, ASTContext &Context, State &S) {
+        if (auto M = BuildMatcherForStmt(Matcher, Actions, Stmt, Context))
+          M(S);
+      };
+    }
+    return
+        [Cache = llvm::DenseMap<const Stmt *, std::function<void(State &)>>(),
+         Matcher = BuildMatcher(), Actions = std::move(Actions)](
+            const Stmt &Stmt, ASTContext &Context, State &S) mutable {
+          auto Res = Cache.try_emplace(&Stmt, nullptr);
+          if (Res.second)
+            Res.first->second =
+                BuildMatcherForStmt(Matcher, Actions, Stmt, Context);
+          if (Res.first->second != nullptr)
+            Res.first->second(S);
+        };
   }
 
 private:
@@ -141,11 +152,42 @@
         std::move(Matchers));
   }
 
+  using Action = std::function<void(
+      const Stmt *, const ast_matchers::MatchFinder::MatchResult &, State &)>;
+
+  /// Binds the `Stmt` and `Context` arguments to `Matcher`, effectively
+  /// returning an action that's only parameterized by `State`. Returns null if
+  /// `Matcher` does not match `Stmt`.
+  static std::function<void(State &)>
+  BuildMatcherForStmt(const ast_matchers::internal::DynTypedMatcher &Matcher,
+                      const std::vector<Action> &Actions, const Stmt &Stmt,
+                      ASTContext &Context) {
+    auto Results = ast_matchers::matchDynamic(Matcher, Stmt, Context);
+    if (Results.empty())
+      return nullptr;
+
+    // Look through the map for the first binding of the form "TagN..." use
+    // that to select the action.
+    for (const auto &Element : Results[0].getMap()) {
+      llvm::StringRef ID(Element.first);
+      size_t Index = 0;
+      if (ID.consume_front("Tag") && !ID.getAsInteger(10, Index) &&
+          Index < Actions.size())
+        return [&Actions, Index, &Stmt, Res = std::move(Results[0]),
+                &Context](State &S) {
+          Actions[Index](
+              &Stmt, ast_matchers::MatchFinder::MatchResult(Res, &Context), S);
+        };
+    }
+
+    llvm_unreachable("Unhandled result in MatchSwitch!");
+  }
+
   std::vector<ast_matchers::internal::DynTypedMatcher> Matchers;
-  std::vector<std::function<void(
-      const Stmt *, const ast_matchers::MatchFinder::MatchResult &, State &)>>
-      Actions;
+  std::vector<Action> Actions;
 };
+
 } // namespace dataflow
 } // namespace clang
+
 #endif // LLVM_CLANG_ANALYSIS_FLOWSENSITIVE_MATCHSWITCH_H_
diff --git a/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp b/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
--- a/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
+++ b/clang/lib/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.cpp
@@ -284,9 +284,6 @@
 }
 
 auto buildTransferMatchSwitch() {
-  // FIXME: Evaluate the efficiency of matchers. If using matchers results in a
-  // lot of duplicated work (e.g. string comparisons), consider providing APIs
-  // that avoid it through memoization.
   return MatchSwitchBuilder<LatticeTransferState>()
       // Attach a symbolic "has_value" state to optional values that we see for
       // the first time.
diff --git a/clang/unittests/Analysis/FlowSensitive/MatchSwitchTest.cpp b/clang/unittests/Analysis/FlowSensitive/MatchSwitchTest.cpp
--- a/clang/unittests/Analysis/FlowSensitive/MatchSwitchTest.cpp
+++ b/clang/unittests/Analysis/FlowSensitive/MatchSwitchTest.cpp
@@ -100,19 +100,26 @@
 }
 
 class TestAnalysis : public DataflowAnalysis<TestAnalysis, BooleanLattice> {
+  bool CacheMatchSwitchActions;
   MatchSwitch<TransferState<BooleanLattice>> TransferSwitch;
 
 public:
+  TestAnalysis(ASTContext &ASTCtx, bool CacheMatchSwitchActions)
+      : DataflowAnalysis<TestAnalysis, BooleanLattice>(ASTCtx),
+        CacheMatchSwitchActions(CacheMatchSwitchActions) {}
+
   explicit TestAnalysis(ASTContext &Context)
       : DataflowAnalysis<TestAnalysis, BooleanLattice>(Context) {
     using namespace ast_matchers;
+    MatchSwitchBuildOptions BuildOptions;
+    BuildOptions.CacheActions = CacheMatchSwitchActions;
     TransferSwitch =
         MatchSwitchBuilder<TransferState<BooleanLattice>>()
             .CaseOf<DeclRefExpr>(declRefExpr(to(varDecl(hasName("X")))),
                                  TransferSetTrue)
             .CaseOf<Stmt>(callExpr(callee(functionDecl(hasName("Foo")))),
                           TransferSetFalse)
-            .Build();
+            .Build(std::move(BuildOptions));
   }
 
   static BooleanLattice initialElement() { return BooleanLattice::bottom(); }
@@ -123,7 +130,7 @@
   }
 };
 
-class MatchSwitchTest : public ::testing::Test {
+class MatchSwitchTest : public ::testing::TestWithParam<bool> {
 protected:
   template <typename Matcher>
   void RunDataflow(llvm::StringRef Code, Matcher Expectations) {
@@ -141,7 +148,10 @@
   }
 };
 
-TEST_F(MatchSwitchTest, JustX) {
+INSTANTIATE_TEST_SUITE_P(MatchSwitchTestInst, MatchSwitchTest,
+                         ::testing::Bool());
+
+TEST_P(MatchSwitchTest, JustX) {
   std::string Code = R"(
     void fun() {
       int X = 1;
@@ -153,7 +163,7 @@
               UnorderedElementsAre(Pair("p", Holds(BooleanLattice(true)))));
 }
 
-TEST_F(MatchSwitchTest, JustFoo) {
+TEST_P(MatchSwitchTest, JustFoo) {
   std::string Code = R"(
     void Foo();
     void fun() {
@@ -165,7 +175,7 @@
               UnorderedElementsAre(Pair("p", Holds(BooleanLattice(false)))));
 }
 
-TEST_F(MatchSwitchTest, XThenFoo) {
+TEST_P(MatchSwitchTest, XThenFoo) {
   std::string Code = R"(
     void Foo();
     void fun() {
@@ -179,7 +189,7 @@
               UnorderedElementsAre(Pair("p", Holds(BooleanLattice(false)))));
 }
 
-TEST_F(MatchSwitchTest, FooThenX) {
+TEST_P(MatchSwitchTest, FooThenX) {
   std::string Code = R"(
     void Foo();
     void fun() {
@@ -193,7 +203,7 @@
               UnorderedElementsAre(Pair("p", Holds(BooleanLattice(true)))));
 }
 
-TEST_F(MatchSwitchTest, Neither) {
+TEST_P(MatchSwitchTest, Neither) {
   std::string Code = R"(
     void Bar();
     void fun(bool b) {