diff --git a/compiler/rustc_ast_lowering/src/lib.rs b/compiler/rustc_ast_lowering/src/lib.rs
index 7a4e39376a86d..b787158c34368 100644
--- a/compiler/rustc_ast_lowering/src/lib.rs
+++ b/compiler/rustc_ast_lowering/src/lib.rs
@@ -48,7 +48,7 @@ use rustc_data_structures::sync::Lrc;
 use rustc_errors::{struct_span_err, Applicability};
 use rustc_hir as hir;
 use rustc_hir::def::{DefKind, Namespace, PartialRes, PerNS, Res};
-use rustc_hir::def_id::{DefId, DefIdMap, LocalDefId, CRATE_DEF_ID};
+use rustc_hir::def_id::{DefId, DefIdMap, DefPathHash, LocalDefId, CRATE_DEF_ID};
 use rustc_hir::definitions::{DefKey, DefPathData, Definitions};
 use rustc_hir::intravisit;
 use rustc_hir::{ConstArg, GenericArg, ParamName};
@@ -59,7 +59,7 @@ use rustc_session::utils::{FlattenNonterminals, NtToTokenstream};
 use rustc_session::Session;
 use rustc_span::edition::Edition;
 use rustc_span::hygiene::ExpnId;
-use rustc_span::source_map::{respan, DesugaringKind};
+use rustc_span::source_map::{respan, CachingSourceMapView, DesugaringKind};
 use rustc_span::symbol::{kw, sym, Ident, Symbol};
 use rustc_span::{Span, DUMMY_SP};
 use rustc_target::spec::abi::Abi;
@@ -204,6 +204,8 @@ pub trait ResolverAstLowering {
 
     fn local_def_id(&self, node: NodeId) -> LocalDefId;
 
+    fn def_path_hash(&self, def_id: DefId) -> DefPathHash;
+
     fn create_def(
         &mut self,
         parent: LocalDefId,
@@ -214,6 +216,32 @@ pub trait ResolverAstLowering {
     ) -> LocalDefId;
 }
 
+struct LoweringHasher<'a> {
+    source_map: CachingSourceMapView<'a>,
+    resolver: &'a dyn ResolverAstLowering,
+}
+
+impl<'a> rustc_span::HashStableContext for LoweringHasher<'a> {
+    #[inline]
+    fn hash_spans(&self) -> bool {
+        true
+    }
+
+    #[inline]
+    fn def_path_hash(&self, def_id: DefId) -> DefPathHash {
+        self.resolver.def_path_hash(def_id)
+    }
+
+    #[inline]
+    fn span_data_to_lines_and_cols(
+        &mut self,
+        span: &rustc_span::SpanData,
+    ) -> Option<(Lrc<rustc_span::SourceFile>, usize, rustc_span::BytePos, usize, rustc_span::BytePos)>
+    {
+        self.source_map.span_data_to_lines_and_cols(span)
+    }
+}
+
 /// Context of `impl Trait` in code, which determines whether it is allowed in an HIR subtree,
 /// and if so, what meaning it has.
 #[derive(Debug)]
@@ -565,6 +593,13 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
         lowered
     }
 
+    fn create_stable_hashing_context(&self) -> LoweringHasher<'_> {
+        LoweringHasher {
+            source_map: CachingSourceMapView::new(self.sess.source_map()),
+            resolver: self.resolver,
+        }
+    }
+
     fn lower_node_id_generic(
         &mut self,
         ast_node_id: NodeId,
@@ -684,7 +719,12 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> {
         span: Span,
         allow_internal_unstable: Option<Lrc<[Symbol]>>,
     ) -> Span {
-        span.mark_with_reason(allow_internal_unstable, reason, self.sess.edition())
+        span.mark_with_reason(
+            allow_internal_unstable,
+            reason,
+            self.sess.edition(),
+            self.create_stable_hashing_context(),
+        )
     }
 
     fn with_anonymous_lifetime_mode<R>(
diff --git a/compiler/rustc_expand/src/expand.rs b/compiler/rustc_expand/src/expand.rs
index f8a12ef8a2081..df673c6084b96 100644
--- a/compiler/rustc_expand/src/expand.rs
+++ b/compiler/rustc_expand/src/expand.rs
@@ -506,7 +506,7 @@ impl<'a, 'b> MacroExpander<'a, 'b> {
                                 .map(|(path, item, _exts)| {
                                     // FIXME: Consider using the derive resolutions (`_exts`)
                                     // instead of enqueuing the derives to be resolved again later.
-                                    let expn_id = ExpnId::fresh(None);
+                                    let expn_id = ExpnId::fresh_empty();
                                     derive_invocations.push((
                                         Invocation {
                                             kind: InvocationKind::Derive { path, item },
@@ -989,7 +989,7 @@ struct InvocationCollector<'a, 'b> {
 
 impl<'a, 'b> InvocationCollector<'a, 'b> {
     fn collect(&mut self, fragment_kind: AstFragmentKind, kind: InvocationKind) -> AstFragment {
-        let expn_id = ExpnId::fresh(None);
+        let expn_id = ExpnId::fresh_empty();
         let vis = kind.placeholder_visibility();
         self.invocations.push((
             Invocation {
diff --git a/compiler/rustc_metadata/src/rmeta/decoder.rs b/compiler/rustc_metadata/src/rmeta/decoder.rs
index 6ecd4a512e9b4..e9773bac819bd 100644
--- a/compiler/rustc_metadata/src/rmeta/decoder.rs
+++ b/compiler/rustc_metadata/src/rmeta/decoder.rs
@@ -393,12 +393,19 @@ impl<'a, 'tcx> Decodable<DecodeContext<'a, 'tcx>> for ExpnId {
                 } else {
                     local_cdata.cstore.get_crate_data(cnum)
                 };
-                Ok(crate_data
+                let expn_data = crate_data
                     .root
                     .expn_data
                     .get(&crate_data, index)
                     .unwrap()
-                    .decode((&crate_data, sess)))
+                    .decode((&crate_data, sess));
+                let expn_hash = crate_data
+                    .root
+                    .expn_hashes
+                    .get(&crate_data, index)
+                    .unwrap()
+                    .decode((&crate_data, sess));
+                Ok((expn_data, expn_hash))
             },
         )
     }
diff --git a/compiler/rustc_metadata/src/rmeta/encoder.rs b/compiler/rustc_metadata/src/rmeta/encoder.rs
index 0e924d644353c..6fac8e595d0ad 100644
--- a/compiler/rustc_metadata/src/rmeta/encoder.rs
+++ b/compiler/rustc_metadata/src/rmeta/encoder.rs
@@ -653,7 +653,7 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> {
         // Therefore, we need to encode the hygiene data last to ensure that we encode
         // any `SyntaxContext`s that might be used.
         i = self.position();
-        let (syntax_contexts, expn_data) = self.encode_hygiene();
+        let (syntax_contexts, expn_data, expn_hashes) = self.encode_hygiene();
         let hygiene_bytes = self.position() - i;
 
         // Encode source_map. This needs to be done last,
@@ -701,6 +701,7 @@ impl<'a, 'tcx> EncodeContext<'a, 'tcx> {
             tables,
             syntax_contexts,
             expn_data,
+            expn_hashes,
         });
 
         let total_bytes = self.position();
@@ -1578,23 +1579,29 @@ impl EncodeContext<'a, 'tcx> {
         self.lazy(foreign_modules.iter().map(|(_, m)| m).cloned())
     }
 
-    fn encode_hygiene(&mut self) -> (SyntaxContextTable, ExpnDataTable) {
+    fn encode_hygiene(&mut self) -> (SyntaxContextTable, ExpnDataTable, ExpnHashTable) {
         let mut syntax_contexts: TableBuilder<_, _> = Default::default();
         let mut expn_data_table: TableBuilder<_, _> = Default::default();
+        let mut expn_hash_table: TableBuilder<_, _> = Default::default();
 
         let _: Result<(), !> = self.hygiene_ctxt.encode(
-            &mut (&mut *self, &mut syntax_contexts, &mut expn_data_table),
-            |(this, syntax_contexts, _), index, ctxt_data| {
+            &mut (&mut *self, &mut syntax_contexts, &mut expn_data_table, &mut expn_hash_table),
+            |(this, syntax_contexts, _, _), index, ctxt_data| {
                 syntax_contexts.set(index, this.lazy(ctxt_data));
                 Ok(())
             },
-            |(this, _, expn_data_table), index, expn_data| {
+            |(this, _, expn_data_table, expn_hash_table), index, expn_data, hash| {
                 expn_data_table.set(index, this.lazy(expn_data));
+                expn_hash_table.set(index, this.lazy(hash));
                 Ok(())
             },
         );
 
-        (syntax_contexts.encode(&mut self.opaque), expn_data_table.encode(&mut self.opaque))
+        (
+            syntax_contexts.encode(&mut self.opaque),
+            expn_data_table.encode(&mut self.opaque),
+            expn_hash_table.encode(&mut self.opaque),
+        )
     }
 
     fn encode_proc_macros(&mut self) -> Option<ProcMacroData> {
diff --git a/compiler/rustc_metadata/src/rmeta/mod.rs b/compiler/rustc_metadata/src/rmeta/mod.rs
index 3793058062347..8cc1935951fe3 100644
--- a/compiler/rustc_metadata/src/rmeta/mod.rs
+++ b/compiler/rustc_metadata/src/rmeta/mod.rs
@@ -21,7 +21,7 @@ use rustc_session::config::SymbolManglingVersion;
 use rustc_span::edition::Edition;
 use rustc_span::hygiene::MacroKind;
 use rustc_span::symbol::{Ident, Symbol};
-use rustc_span::{self, ExpnData, ExpnId, Span};
+use rustc_span::{self, ExpnData, ExpnHash, ExpnId, Span};
 use rustc_target::spec::{PanicStrategy, TargetTriple};
 
 use std::marker::PhantomData;
@@ -171,6 +171,7 @@ macro_rules! Lazy {
 
 type SyntaxContextTable = Lazy<Table<u32, Lazy<SyntaxContextData>>>;
 type ExpnDataTable = Lazy<Table<u32, Lazy<ExpnData>>>;
+type ExpnHashTable = Lazy<Table<u32, Lazy<ExpnHash>>>;
 
 #[derive(MetadataEncodable, MetadataDecodable)]
 crate struct ProcMacroData {
@@ -226,6 +227,7 @@ crate struct CrateRoot<'tcx> {
 
     syntax_contexts: SyntaxContextTable,
     expn_data: ExpnDataTable,
+    expn_hashes: ExpnHashTable,
 
     source_map: Lazy<[rustc_span::SourceFile]>,
 
diff --git a/compiler/rustc_middle/src/ich/hcx.rs b/compiler/rustc_middle/src/ich/hcx.rs
index f1c4529b8552b..32ccdafaeb48c 100644
--- a/compiler/rustc_middle/src/ich/hcx.rs
+++ b/compiler/rustc_middle/src/ich/hcx.rs
@@ -16,7 +16,6 @@ use rustc_span::{BytePos, CachingSourceMapView, SourceFile, SpanData};
 
 use smallvec::SmallVec;
 use std::cmp::Ord;
-use std::thread::LocalKey;
 
 fn compute_ignored_attr_names() -> FxHashSet<Symbol> {
     debug_assert!(!ich::IGNORED_ATTRIBUTES.is_empty());
@@ -230,13 +229,6 @@ impl<'a> rustc_span::HashStableContext for StableHashingContext<'a> {
         self.def_path_hash(def_id)
     }
 
-    fn expn_id_cache() -> &'static LocalKey<rustc_span::ExpnIdCache> {
-        thread_local! {
-            static CACHE: rustc_span::ExpnIdCache = Default::default();
-        }
-        &CACHE
-    }
-
     fn span_data_to_lines_and_cols(
         &mut self,
         span: &SpanData,
diff --git a/compiler/rustc_middle/src/ty/query/on_disk_cache.rs b/compiler/rustc_middle/src/ty/query/on_disk_cache.rs
index 15188643d6631..e3db0d2cf30a6 100644
--- a/compiler/rustc_middle/src/ty/query/on_disk_cache.rs
+++ b/compiler/rustc_middle/src/ty/query/on_disk_cache.rs
@@ -25,7 +25,7 @@ use rustc_span::hygiene::{
 };
 use rustc_span::source_map::{SourceMap, StableSourceFileId};
 use rustc_span::CachingSourceMapView;
-use rustc_span::{BytePos, ExpnData, SourceFile, Span, DUMMY_SP};
+use rustc_span::{BytePos, ExpnData, ExpnHash, SourceFile, Span, DUMMY_SP};
 use std::collections::hash_map::Entry;
 use std::mem;
 
@@ -364,9 +364,9 @@ impl<'sess> OnDiskCache<'sess> {
                     syntax_contexts.insert(index, pos);
                     Ok(())
                 },
-                |encoder, index, expn_data| -> FileEncodeResult {
+                |encoder, index, expn_data, hash| -> FileEncodeResult {
                     let pos = AbsoluteBytePos::new(encoder.position());
-                    encoder.encode_tagged(TAG_EXPN_DATA, expn_data)?;
+                    encoder.encode_tagged(TAG_EXPN_DATA, &(expn_data, hash))?;
                     expn_ids.insert(index, pos);
                     Ok(())
                 },
@@ -804,7 +804,7 @@ impl<'a, 'tcx> Decodable<CacheDecoder<'a, 'tcx>> for ExpnId {
                     .unwrap_or_else(|| panic!("Bad index {:?} (map {:?})", index, expn_data));
 
                 this.with_position(pos.to_usize(), |decoder| {
-                    let data: ExpnData = decode_tagged(decoder, TAG_EXPN_DATA)?;
+                    let data: (ExpnData, ExpnHash) = decode_tagged(decoder, TAG_EXPN_DATA)?;
                     Ok(data)
                 })
             },
diff --git a/compiler/rustc_mir/src/transform/inline.rs b/compiler/rustc_mir/src/transform/inline.rs
index 703ac39dc3080..7d765cec57578 100644
--- a/compiler/rustc_mir/src/transform/inline.rs
+++ b/compiler/rustc_mir/src/transform/inline.rs
@@ -839,7 +839,8 @@ impl<'a, 'tcx> MutVisitor<'tcx> for Integrator<'a, 'tcx> {
             ExpnData::default(ExpnKind::Inlined, *span, self.tcx.sess.edition(), None, None);
         expn_data.def_site = self.body_span;
         // Make sure that all spans track the fact that they were inlined.
-        *span = self.callsite_span.fresh_expansion(expn_data);
+        *span =
+            self.callsite_span.fresh_expansion(expn_data, self.tcx.create_stable_hashing_context());
     }
 
     fn visit_place(&mut self, place: &mut Place<'tcx>, context: PlaceContext, location: Location) {
diff --git a/compiler/rustc_resolve/src/lib.rs b/compiler/rustc_resolve/src/lib.rs
index bcdae1cb43dbd..4d12415215194 100644
--- a/compiler/rustc_resolve/src/lib.rs
+++ b/compiler/rustc_resolve/src/lib.rs
@@ -39,7 +39,7 @@ use rustc_errors::{struct_span_err, Applicability, DiagnosticBuilder};
 use rustc_expand::base::{DeriveResolutions, SyntaxExtension, SyntaxExtensionKind};
 use rustc_hir::def::Namespace::*;
 use rustc_hir::def::{self, CtorOf, DefKind, NonMacroAttrKind, PartialRes};
-use rustc_hir::def_id::{CrateNum, DefId, DefIdMap, LocalDefId, CRATE_DEF_INDEX};
+use rustc_hir::def_id::{CrateNum, DefId, DefIdMap, DefPathHash, LocalDefId, CRATE_DEF_INDEX};
 use rustc_hir::definitions::{DefKey, DefPathData, Definitions};
 use rustc_hir::TraitCandidate;
 use rustc_index::vec::IndexVec;
@@ -54,7 +54,7 @@ use rustc_session::lint::{BuiltinLintDiagnostics, LintBuffer};
 use rustc_session::Session;
 use rustc_span::edition::Edition;
 use rustc_span::hygiene::{ExpnId, ExpnKind, MacroKind, SyntaxContext, Transparency};
-use rustc_span::source_map::Spanned;
+use rustc_span::source_map::{CachingSourceMapView, Spanned};
 use rustc_span::symbol::{kw, sym, Ident, Symbol};
 use rustc_span::{Span, DUMMY_SP};
 
@@ -1149,6 +1149,13 @@ impl ResolverAstLowering for Resolver<'_> {
         self.opt_local_def_id(node).unwrap_or_else(|| panic!("no entry for node id: `{:?}`", node))
     }
 
+    fn def_path_hash(&self, def_id: DefId) -> DefPathHash {
+        match def_id.as_local() {
+            Some(def_id) => self.definitions.def_path_hash(def_id),
+            None => self.cstore().def_path_hash(def_id),
+        }
+    }
+
     /// Adds a definition with a parent definition.
     fn create_def(
         &mut self,
@@ -1192,6 +1199,32 @@ impl ResolverAstLowering for Resolver<'_> {
     }
 }
 
+struct ExpandHasher<'a, 'b> {
+    source_map: CachingSourceMapView<'a>,
+    resolver: &'a Resolver<'b>,
+}
+
+impl<'a, 'b> rustc_span::HashStableContext for ExpandHasher<'a, 'b> {
+    #[inline]
+    fn hash_spans(&self) -> bool {
+        true
+    }
+
+    #[inline]
+    fn def_path_hash(&self, def_id: DefId) -> DefPathHash {
+        self.resolver.def_path_hash(def_id)
+    }
+
+    #[inline]
+    fn span_data_to_lines_and_cols(
+        &mut self,
+        span: &rustc_span::SpanData,
+    ) -> Option<(Lrc<rustc_span::SourceFile>, usize, rustc_span::BytePos, usize, rustc_span::BytePos)>
+    {
+        self.source_map.span_data_to_lines_and_cols(span)
+    }
+}
+
 impl<'a> Resolver<'a> {
     pub fn new(
         session: &'a Session,
@@ -1364,6 +1397,13 @@ impl<'a> Resolver<'a> {
         resolver
     }
 
+    fn create_stable_hashing_context(&self) -> ExpandHasher<'_, 'a> {
+        ExpandHasher {
+            source_map: CachingSourceMapView::new(self.session.source_map()),
+            resolver: self,
+        }
+    }
+
     pub fn next_node_id(&mut self) -> NodeId {
         let next = self
             .next_node_id
diff --git a/compiler/rustc_resolve/src/macros.rs b/compiler/rustc_resolve/src/macros.rs
index e024ade7b3c84..8686704388fee 100644
--- a/compiler/rustc_resolve/src/macros.rs
+++ b/compiler/rustc_resolve/src/macros.rs
@@ -218,14 +218,17 @@ impl<'a> ResolverExpand for Resolver<'a> {
         parent_module_id: Option<NodeId>,
     ) -> ExpnId {
         let parent_module = parent_module_id.map(|module_id| self.local_def_id(module_id));
-        let expn_id = ExpnId::fresh(Some(ExpnData::allow_unstable(
-            ExpnKind::AstPass(pass),
-            call_site,
-            self.session.edition(),
-            features.into(),
-            None,
-            parent_module.map(LocalDefId::to_def_id),
-        )));
+        let expn_id = ExpnId::fresh(
+            ExpnData::allow_unstable(
+                ExpnKind::AstPass(pass),
+                call_site,
+                self.session.edition(),
+                features.into(),
+                None,
+                parent_module.map(LocalDefId::to_def_id),
+            ),
+            self.create_stable_hashing_context(),
+        );
 
         let parent_scope = parent_module
             .map_or(self.empty_module, |parent_def_id| self.module_map[&parent_def_id]);
@@ -287,15 +290,18 @@ impl<'a> ResolverExpand for Resolver<'a> {
         )?;
 
         let span = invoc.span();
-        invoc_id.set_expn_data(ext.expn_data(
-            parent_scope.expansion,
-            span,
-            fast_print_path(path),
-            res.opt_def_id(),
-            res.opt_def_id().map(|macro_def_id| {
-                self.macro_def_scope_from_def_id(macro_def_id).nearest_parent_mod
-            }),
-        ));
+        invoc_id.set_expn_data(
+            ext.expn_data(
+                parent_scope.expansion,
+                span,
+                fast_print_path(path),
+                res.opt_def_id(),
+                res.opt_def_id().map(|macro_def_id| {
+                    self.macro_def_scope_from_def_id(macro_def_id).nearest_parent_mod
+                }),
+            ),
+            self.create_stable_hashing_context(),
+        );
 
         if let Res::Def(_, _) = res {
             // Gate macro attributes in `#[derive]` output.
diff --git a/compiler/rustc_span/src/hygiene.rs b/compiler/rustc_span/src/hygiene.rs
index 78b181aa3300a..9a101169d20b6 100644
--- a/compiler/rustc_span/src/hygiene.rs
+++ b/compiler/rustc_span/src/hygiene.rs
@@ -27,18 +27,18 @@
 use crate::edition::Edition;
 use crate::symbol::{kw, sym, Symbol};
 use crate::with_session_globals;
-use crate::{BytePos, CachingSourceMapView, ExpnIdCache, SourceFile, Span, DUMMY_SP};
+use crate::{HashStableContext, Span, DUMMY_SP};
 
-use crate::def_id::{CrateNum, DefId, DefPathHash, CRATE_DEF_INDEX, LOCAL_CRATE};
+use crate::def_id::{CrateNum, DefId, CRATE_DEF_INDEX, LOCAL_CRATE};
 use rustc_data_structures::fingerprint::Fingerprint;
 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
 use rustc_data_structures::stable_hasher::{HashStable, StableHasher};
 use rustc_data_structures::sync::{Lock, Lrc};
+use rustc_data_structures::unhash::UnhashMap;
 use rustc_macros::HashStable_Generic;
 use rustc_serialize::{Decodable, Decoder, Encodable, Encoder};
 use std::fmt;
 use std::hash::Hash;
-use std::thread::LocalKey;
 use tracing::*;
 
 /// A `SyntaxContext` represents a chain of pairs `(ExpnId, Transparency)` named "marks".
@@ -62,6 +62,10 @@ pub struct SyntaxContextData {
 #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
 pub struct ExpnId(u32);
 
+/// A unique hash value associated to an expansion.
+#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encodable, Decodable, HashStable_Generic)]
+pub struct ExpnHash(Fingerprint);
+
 /// A property of a macro expansion that determines how identifiers
 /// produced by that expansion are resolved.
 #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Hash, Debug, Encodable, Decodable)]
@@ -83,12 +87,13 @@ pub enum Transparency {
 }
 
 impl ExpnId {
-    pub fn fresh(expn_data: Option<ExpnData>) -> Self {
-        let has_data = expn_data.is_some();
-        let expn_id = HygieneData::with(|data| data.fresh_expn(expn_data));
-        if has_data {
-            update_disambiguator(expn_id);
-        }
+    pub fn fresh_empty() -> Self {
+        HygieneData::with(|data| data.fresh_expn(None))
+    }
+
+    pub fn fresh(expn_data: ExpnData, ctx: impl HashStableContext) -> Self {
+        let expn_id = HygieneData::with(|data| data.fresh_expn(Some(expn_data)));
+        update_disambiguator(expn_id, ctx);
         expn_id
     }
 
@@ -108,13 +113,23 @@ impl ExpnId {
         ExpnId(raw)
     }
 
+    #[inline]
+    pub fn expn_hash(self) -> ExpnHash {
+        HygieneData::with(|data| data.expn_hash(self))
+    }
+
+    #[inline]
+    pub fn from_hash(hash: ExpnHash) -> Option<ExpnId> {
+        HygieneData::with(|data| data.expn_hash_to_expn_id.get(&hash).copied())
+    }
+
     #[inline]
     pub fn expn_data(self) -> ExpnData {
         HygieneData::with(|data| data.expn_data(self).clone())
     }
 
     #[inline]
-    pub fn set_expn_data(self, mut expn_data: ExpnData) {
+    pub fn set_expn_data(self, mut expn_data: ExpnData, ctx: impl HashStableContext) {
         HygieneData::with(|data| {
             let old_expn_data = &mut data.expn_data[self.0 as usize];
             assert!(old_expn_data.is_none(), "expansion data is reset for an expansion ID");
@@ -122,7 +137,7 @@ impl ExpnId {
             expn_data.orig_id = Some(self.as_u32());
             *old_expn_data = Some(expn_data);
         });
-        update_disambiguator(self)
+        update_disambiguator(self, ctx)
     }
 
     pub fn is_descendant_of(self, ancestor: ExpnId) -> bool {
@@ -161,6 +176,8 @@ pub struct HygieneData {
     /// between creation of an expansion ID and obtaining its data (e.g. macros are collected
     /// first and then resolved later), so we use an `Option` here.
     expn_data: Vec<Option<ExpnData>>,
+    expn_hashes: Vec<ExpnHash>,
+    expn_hash_to_expn_id: UnhashMap<ExpnHash, ExpnId>,
     syntax_context_data: Vec<SyntaxContextData>,
     syntax_context_map: FxHashMap<(SyntaxContext, ExpnId, Transparency), SyntaxContext>,
     /// Maps the `Fingerprint` of an `ExpnData` to the next disambiguator value.
@@ -184,6 +201,9 @@ impl HygieneData {
 
         HygieneData {
             expn_data: vec![Some(root_data)],
+            expn_hashes: vec![ExpnHash(Fingerprint::ZERO)],
+            expn_hash_to_expn_id: std::iter::once((ExpnHash(Fingerprint::ZERO), ExpnId(0)))
+                .collect(),
             syntax_context_data: vec![SyntaxContextData {
                 outer_expn: ExpnId::root(),
                 outer_transparency: Transparency::Opaque,
@@ -208,9 +228,15 @@ impl HygieneData {
             data.orig_id = Some(raw_id);
         }
         self.expn_data.push(expn_data);
+        self.expn_hashes.push(ExpnHash(Fingerprint::ZERO));
         ExpnId(raw_id)
     }
 
+    #[inline]
+    fn expn_hash(&self, expn_id: ExpnId) -> ExpnHash {
+        self.expn_hashes[expn_id.0 as usize]
+    }
+
     fn expn_data(&self, expn_id: ExpnId) -> &ExpnData {
         self.expn_data[expn_id.0 as usize].as_ref().expect("no expansion data for an expansion ID")
     }
@@ -660,16 +686,17 @@ impl Span {
     /// other compiler-generated code to set per-span properties like allowed unstable features.
     /// The returned span belongs to the created expansion and has the new properties,
     /// but its location is inherited from the current span.
-    pub fn fresh_expansion(self, expn_data: ExpnData) -> Span {
-        self.fresh_expansion_with_transparency(expn_data, Transparency::Transparent)
+    pub fn fresh_expansion(self, expn_data: ExpnData, ctx: impl HashStableContext) -> Span {
+        self.fresh_expansion_with_transparency(expn_data, Transparency::Transparent, ctx)
     }
 
     pub fn fresh_expansion_with_transparency(
         self,
         expn_data: ExpnData,
         transparency: Transparency,
+        ctx: impl HashStableContext,
     ) -> Span {
-        let expn_id = ExpnId::fresh(Some(expn_data));
+        let expn_id = ExpnId::fresh(expn_data, ctx);
         HygieneData::with(|data| {
             self.with_ctxt(data.apply_mark(SyntaxContext::root(), expn_id, transparency))
         })
@@ -682,11 +709,13 @@ impl Span {
         allow_internal_unstable: Option<Lrc<[Symbol]>>,
         reason: DesugaringKind,
         edition: Edition,
+        ctx: impl HashStableContext,
     ) -> Span {
-        self.fresh_expansion(ExpnData {
+        let expn_data = ExpnData {
             allow_internal_unstable,
             ..ExpnData::default(ExpnKind::Desugaring(reason), self, edition, None, None)
-        })
+        };
+        self.fresh_expansion(expn_data, ctx)
     }
 }
 
@@ -838,6 +867,13 @@ impl ExpnData {
     pub fn is_root(&self) -> bool {
         matches!(self.kind, ExpnKind::Root)
     }
+
+    #[inline]
+    fn hash_expn(&self, ctx: &mut impl HashStableContext) -> Fingerprint {
+        let mut hasher = StableHasher::new();
+        self.hash_stable(ctx, &mut hasher);
+        hasher.finish()
+    }
 }
 
 /// Expansion kind.
@@ -984,16 +1020,11 @@ pub struct HygieneEncodeContext {
 }
 
 impl HygieneEncodeContext {
-    pub fn encode<
-        T,
-        R,
-        F: FnMut(&mut T, u32, &SyntaxContextData) -> Result<(), R>,
-        G: FnMut(&mut T, u32, &ExpnData) -> Result<(), R>,
-    >(
+    pub fn encode<T, R>(
         &self,
         encoder: &mut T,
-        mut encode_ctxt: F,
-        mut encode_expn: G,
+        mut encode_ctxt: impl FnMut(&mut T, u32, &SyntaxContextData) -> Result<(), R>,
+        mut encode_expn: impl FnMut(&mut T, u32, ExpnData, ExpnHash) -> Result<(), R>,
     ) -> Result<(), R> {
         // When we serialize a `SyntaxContextData`, we may end up serializing
         // a `SyntaxContext` that we haven't seen before
@@ -1011,7 +1042,7 @@ impl HygieneEncodeContext {
             // It's fine to iterate over a HashMap, because the serialization
             // of the table that we insert data into doesn't depend on insertion
             // order
-            for_all_ctxts_in(latest_ctxts.into_iter(), |(index, ctxt, data)| {
+            for_all_ctxts_in(latest_ctxts.into_iter(), |index, ctxt, data| {
                 if self.serialized_ctxts.lock().insert(ctxt) {
                     encode_ctxt(encoder, index, data)?;
                 }
@@ -1020,9 +1051,9 @@ impl HygieneEncodeContext {
 
             let latest_expns = { std::mem::take(&mut *self.latest_expns.lock()) };
 
-            for_all_expns_in(latest_expns.into_iter(), |index, expn, data| {
+            for_all_expns_in(latest_expns.into_iter(), |index, expn, data, hash| {
                 if self.serialized_expns.lock().insert(expn) {
-                    encode_expn(encoder, index, data)?;
+                    encode_expn(encoder, index, data, hash)?;
                 }
                 Ok(())
             })?;
@@ -1045,16 +1076,14 @@ pub struct HygieneDecodeContext {
     remapped_expns: Lock<Vec<Option<ExpnId>>>,
 }
 
-pub fn decode_expn_id<
-    'a,
-    D: Decoder,
-    F: FnOnce(&mut D, u32) -> Result<ExpnData, D::Error>,
-    G: FnOnce(CrateNum) -> &'a HygieneDecodeContext,
->(
+pub fn decode_expn_id<'a, D: Decoder, G>(
     d: &mut D,
     mode: ExpnDataDecodeMode<'a, G>,
-    decode_data: F,
-) -> Result<ExpnId, D::Error> {
+    decode_data: impl FnOnce(&mut D, u32) -> Result<(ExpnData, ExpnHash), D::Error>,
+) -> Result<ExpnId, D::Error>
+where
+    G: FnOnce(CrateNum) -> &'a HygieneDecodeContext,
+{
     let index = u32::decode(d)?;
     let context = match mode {
         ExpnDataDecodeMode::IncrComp(context) => context,
@@ -1082,9 +1111,13 @@ pub fn decode_expn_id<
 
     // Don't decode the data inside `HygieneData::with`, since we need to recursively decode
     // other ExpnIds
-    let mut expn_data = decode_data(d, index)?;
+    let (mut expn_data, hash) = decode_data(d, index)?;
 
     let expn_id = HygieneData::with(|hygiene_data| {
+        if let Some(&expn_id) = hygiene_data.expn_hash_to_expn_id.get(&hash) {
+            return expn_id;
+        }
+
         let expn_id = ExpnId(hygiene_data.expn_data.len() as u32);
 
         // If we just deserialized an `ExpnData` owned by
@@ -1097,6 +1130,9 @@ pub fn decode_expn_id<
         }
 
         hygiene_data.expn_data.push(Some(expn_data));
+        hygiene_data.expn_hashes.push(hash);
+        let _old_id = hygiene_data.expn_hash_to_expn_id.insert(hash, expn_id);
+        debug_assert!(_old_id.is_none());
 
         let mut expns = outer_expns.lock();
         let new_len = index as usize + 1;
@@ -1183,7 +1219,7 @@ pub fn decode_syntax_context<
     Ok(new_ctxt)
 }
 
-fn for_all_ctxts_in<E, F: FnMut((u32, SyntaxContext, &SyntaxContextData)) -> Result<(), E>>(
+fn for_all_ctxts_in<E, F: FnMut(u32, SyntaxContext, &SyntaxContextData) -> Result<(), E>>(
     ctxts: impl Iterator<Item = SyntaxContext>,
     mut f: F,
 ) -> Result<(), E> {
@@ -1191,20 +1227,26 @@ fn for_all_ctxts_in<E, F: FnMut((u32, SyntaxContext, &SyntaxContextData)) -> Res
         ctxts.map(|ctxt| (ctxt, data.syntax_context_data[ctxt.0 as usize].clone())).collect()
     });
     for (ctxt, data) in all_data.into_iter() {
-        f((ctxt.0, ctxt, &data))?;
+        f(ctxt.0, ctxt, &data)?;
     }
     Ok(())
 }
 
-fn for_all_expns_in<E, F: FnMut(u32, ExpnId, &ExpnData) -> Result<(), E>>(
+fn for_all_expns_in<E>(
     expns: impl Iterator<Item = ExpnId>,
-    mut f: F,
+    mut f: impl FnMut(u32, ExpnId, ExpnData, ExpnHash) -> Result<(), E>,
 ) -> Result<(), E> {
     let all_data: Vec<_> = HygieneData::with(|data| {
-        expns.map(|expn| (expn, data.expn_data[expn.0 as usize].clone())).collect()
+        expns
+            .map(|expn| {
+                let idx = expn.0 as usize;
+                (expn, data.expn_data[idx].clone(), data.expn_hashes[idx].clone())
+            })
+            .collect()
     });
-    for (expn, data) in all_data.into_iter() {
-        f(expn.0, expn, &data.unwrap_or_else(|| panic!("Missing data for {:?}", expn)))?;
+    for (expn, data, hash) in all_data.into_iter() {
+        let data = data.unwrap_or_else(|| panic!("Missing data for {:?}", expn));
+        f(expn.0, expn, data, hash)?;
     }
     Ok(())
 }
@@ -1305,105 +1347,78 @@ impl<D: Decoder> Decodable<D> for SyntaxContext {
 /// `set_expn_data`). It is *not* called for foreign `ExpnId`s deserialized
 /// from another crate's metadata - since `ExpnData` includes a `krate` field,
 /// collisions are only possible between `ExpnId`s within the same crate.
-fn update_disambiguator(expn_id: ExpnId) {
-    /// A `HashStableContext` which hashes the raw id values for `DefId`
-    /// and `CrateNum`, rather than using their computed stable hash.
-    ///
-    /// This allows us to use the `HashStable` implementation on `ExpnId`
-    /// early on in compilation, before we've constructed a `TyCtxt`.
-    /// The `Fingerprint`s created by this context are not 'stable', since
-    /// the raw `CrateNum` and `DefId` values for an item may change between
-    /// sessions due to unrelated changes (e.g. adding/removing an different item).
-    ///
-    /// However, this is fine for our purposes - we only need to detect
-    /// when two `ExpnData`s have the same `Fingerprint`. Since the hashes produced
-    /// by this context still obey the properties of `HashStable`, we have
-    /// that
-    /// `hash_stable(expn1, DummyHashStableContext) == hash_stable(expn2, DummyHashStableContext)`
-    /// iff `hash_stable(expn1, StableHashingContext) == hash_stable(expn2, StableHasingContext)`.
-    ///
-    /// This is sufficient for determining when we need to update the disambiguator.
-    struct DummyHashStableContext<'a> {
-        caching_source_map: CachingSourceMapView<'a>,
-    }
-
-    impl<'a> crate::HashStableContext for DummyHashStableContext<'a> {
-        #[inline]
-        fn def_path_hash(&self, def_id: DefId) -> DefPathHash {
-            DefPathHash(Fingerprint::new(
-                def_id.krate.as_u32().into(),
-                def_id.index.as_u32().into(),
-            ))
-        }
-
-        fn expn_id_cache() -> &'static LocalKey<ExpnIdCache> {
-            // This cache is only used by `DummyHashStableContext`,
-            // so we won't pollute the cache values of the normal `StableHashingContext`
-            thread_local! {
-                static CACHE: ExpnIdCache = const { ExpnIdCache::new(Vec::new()) };
-            }
-
-            &CACHE
-        }
-
-        fn hash_spans(&self) -> bool {
-            true
-        }
-        fn span_data_to_lines_and_cols(
-            &mut self,
-            span: &crate::SpanData,
-        ) -> Option<(Lrc<SourceFile>, usize, BytePos, usize, BytePos)> {
-            self.caching_source_map.span_data_to_lines_and_cols(span)
-        }
-    }
-
-    let source_map = with_session_globals(|session_globals| {
-        session_globals.source_map.borrow().as_ref().unwrap().clone()
-    });
-
-    let mut ctx =
-        DummyHashStableContext { caching_source_map: CachingSourceMapView::new(&source_map) };
-
-    let mut hasher = StableHasher::new();
-
-    let expn_data = expn_id.expn_data();
+fn update_disambiguator(expn_id: ExpnId, mut ctx: impl HashStableContext) {
+    let mut expn_data = expn_id.expn_data();
     // This disambiguator should not have been set yet.
     assert_eq!(
         expn_data.disambiguator, 0,
         "Already set disambiguator for ExpnData: {:?}",
         expn_data
     );
-    expn_data.hash_stable(&mut ctx, &mut hasher);
-    let first_hash = hasher.finish();
+    let mut expn_hash = expn_data.hash_expn(&mut ctx);
 
-    let modified = HygieneData::with(|data| {
+    let disambiguator = HygieneData::with(|data| {
         // If this is the first ExpnData with a given hash, then keep our
         // disambiguator at 0 (the default u32 value)
-        let disambig = data.expn_data_disambiguators.entry(first_hash).or_default();
-        data.expn_data[expn_id.0 as usize].as_mut().unwrap().disambiguator = *disambig;
+        let disambig = data.expn_data_disambiguators.entry(expn_hash).or_default();
+        let disambiguator = *disambig;
         *disambig += 1;
-
-        *disambig != 1
+        disambiguator
     });
 
-    if modified {
-        debug!("Set disambiguator for {:?} (hash {:?})", expn_id, first_hash);
-        debug!("expn_data = {:?}", expn_id.expn_data());
+    if disambiguator != 0 {
+        debug!("Set disambiguator for {:?} (hash {:?})", expn_id, expn_hash);
+        debug!("expn_data = {:?}", expn_data);
+
+        expn_data.disambiguator = disambiguator;
+        expn_hash = expn_data.hash_expn(&mut ctx);
 
         // Verify that the new disambiguator makes the hash unique
         #[cfg(debug_assertions)]
-        {
-            hasher = StableHasher::new();
-            expn_id.expn_data().hash_stable(&mut ctx, &mut hasher);
-            let new_hash: Fingerprint = hasher.finish();
-
-            HygieneData::with(|data| {
-                assert_eq!(
-                    data.expn_data_disambiguators.get(&new_hash),
-                    None,
-                    "Hash collision after disambiguator update!",
-                );
-            });
+        HygieneData::with(|data| {
+            assert_eq!(
+                data.expn_data_disambiguators.get(&expn_hash),
+                None,
+                "Hash collision after disambiguator update!",
+            );
+        });
+    }
+
+    let expn_hash = ExpnHash(expn_hash);
+    HygieneData::with(|data| {
+        data.expn_data[expn_id.0 as usize].as_mut().unwrap().disambiguator = disambiguator;
+        debug_assert_eq!(data.expn_hashes[expn_id.0 as usize].0, Fingerprint::ZERO);
+        data.expn_hashes[expn_id.0 as usize] = expn_hash;
+        let _old_id = data.expn_hash_to_expn_id.insert(expn_hash, expn_id);
+        debug_assert!(_old_id.is_none());
+    });
+}
+
+impl<CTX: HashStableContext> HashStable<CTX> for SyntaxContext {
+    fn hash_stable(&self, ctx: &mut CTX, hasher: &mut StableHasher) {
+        const TAG_EXPANSION: u8 = 0;
+        const TAG_NO_EXPANSION: u8 = 1;
+
+        if *self == SyntaxContext::root() {
+            TAG_NO_EXPANSION.hash_stable(ctx, hasher);
+        } else {
+            TAG_EXPANSION.hash_stable(ctx, hasher);
+            let (expn_id, transparency) = self.outer_mark();
+            expn_id.hash_stable(ctx, hasher);
+            transparency.hash_stable(ctx, hasher);
+        }
+    }
+}
+
+impl<CTX: HashStableContext> HashStable<CTX> for ExpnId {
+    fn hash_stable(&self, ctx: &mut CTX, hasher: &mut StableHasher) {
+        let hash = if *self == ExpnId::root() {
+            // Avoid fetching TLS storage for a trivial often-used value.
+            Fingerprint::ZERO
+        } else {
+            self.expn_hash().0
         };
+
+        hash.hash_stable(ctx, hasher);
     }
 }
diff --git a/compiler/rustc_span/src/lib.rs b/compiler/rustc_span/src/lib.rs
index 84bef4b113c15..7a1ee20ee7951 100644
--- a/compiler/rustc_span/src/lib.rs
+++ b/compiler/rustc_span/src/lib.rs
@@ -36,9 +36,9 @@ use source_map::SourceMap;
 pub mod edition;
 use edition::Edition;
 pub mod hygiene;
-pub use hygiene::SyntaxContext;
 use hygiene::Transparency;
-pub use hygiene::{DesugaringKind, ExpnData, ExpnId, ExpnKind, ForLoopLoc, MacroKind};
+pub use hygiene::{DesugaringKind, ExpnKind, ForLoopLoc, MacroKind};
+pub use hygiene::{ExpnData, ExpnHash, ExpnId, SyntaxContext};
 pub mod def_id;
 use def_id::{CrateNum, DefId, DefPathHash, LOCAL_CRATE};
 pub mod lev_distance;
@@ -51,19 +51,16 @@ pub use symbol::{sym, Symbol};
 mod analyze_source_file;
 pub mod fatal_error;
 
-use rustc_data_structures::fingerprint::Fingerprint;
 use rustc_data_structures::stable_hasher::{HashStable, StableHasher};
 use rustc_data_structures::sync::{Lock, Lrc};
 
 use std::borrow::Cow;
-use std::cell::RefCell;
 use std::cmp::{self, Ordering};
 use std::fmt;
 use std::hash::Hash;
 use std::ops::{Add, Range, Sub};
 use std::path::{Path, PathBuf};
 use std::str::FromStr;
-use std::thread::LocalKey;
 
 use md5::Md5;
 use sha1::Digest;
@@ -1958,11 +1955,6 @@ impl InnerSpan {
 /// instead of implementing everything in rustc_middle.
 pub trait HashStableContext {
     fn def_path_hash(&self, def_id: DefId) -> DefPathHash;
-    /// Obtains a cache for storing the `Fingerprint` of an `ExpnId`.
-    /// This method allows us to have multiple `HashStableContext` implementations
-    /// that hash things in a different way, without the results of one polluting
-    /// the cache of the other.
-    fn expn_id_cache() -> &'static LocalKey<ExpnIdCache>;
     fn hash_spans(&self) -> bool;
     fn span_data_to_lines_and_cols(
         &mut self,
@@ -2036,60 +2028,3 @@ where
         Hash::hash(&len, hasher);
     }
 }
-
-impl<CTX: HashStableContext> HashStable<CTX> for SyntaxContext {
-    fn hash_stable(&self, ctx: &mut CTX, hasher: &mut StableHasher) {
-        const TAG_EXPANSION: u8 = 0;
-        const TAG_NO_EXPANSION: u8 = 1;
-
-        if *self == SyntaxContext::root() {
-            TAG_NO_EXPANSION.hash_stable(ctx, hasher);
-        } else {
-            TAG_EXPANSION.hash_stable(ctx, hasher);
-            let (expn_id, transparency) = self.outer_mark();
-            expn_id.hash_stable(ctx, hasher);
-            transparency.hash_stable(ctx, hasher);
-        }
-    }
-}
-
-pub type ExpnIdCache = RefCell<Vec<Option<Fingerprint>>>;
-
-impl<CTX: HashStableContext> HashStable<CTX> for ExpnId {
-    fn hash_stable(&self, ctx: &mut CTX, hasher: &mut StableHasher) {
-        const TAG_ROOT: u8 = 0;
-        const TAG_NOT_ROOT: u8 = 1;
-
-        if *self == ExpnId::root() {
-            TAG_ROOT.hash_stable(ctx, hasher);
-            return;
-        }
-
-        // Since the same expansion context is usually referenced many
-        // times, we cache a stable hash of it and hash that instead of
-        // recursing every time.
-        let index = self.as_u32() as usize;
-        let res = CTX::expn_id_cache().with(|cache| cache.borrow().get(index).copied().flatten());
-
-        if let Some(res) = res {
-            res.hash_stable(ctx, hasher);
-        } else {
-            let new_len = index + 1;
-
-            let mut sub_hasher = StableHasher::new();
-            TAG_NOT_ROOT.hash_stable(ctx, &mut sub_hasher);
-            self.expn_data().hash_stable(ctx, &mut sub_hasher);
-            let sub_hash: Fingerprint = sub_hasher.finish();
-
-            CTX::expn_id_cache().with(|cache| {
-                let mut cache = cache.borrow_mut();
-                if cache.len() < new_len {
-                    cache.resize(new_len, None);
-                }
-                let prev = cache[index].replace(sub_hash);
-                assert_eq!(prev, None, "Cache slot was filled");
-            });
-            sub_hash.hash_stable(ctx, hasher);
-        }
-    }
-}