diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,12 @@
 # Changelog
 
+- 0.2.3 (2026-07-06)
+  * The constant-time 'Eq' on 'MAC' now folds over the two buffers
+    directly, rather than materialising an intermediate ByteString
+    holding their XOR on the heap. Semantics are unchanged.
+
+  * Improves the performance of constant-time MAC comparison.
+
 - 0.2.2 (2026-02-01)
   * The library has been refactored substantially to achieve greater
     control over heap allocation, particularly around HMAC calculation.
diff --git a/lib/Crypto/Hash/SHA512/Internal.hs b/lib/Crypto/Hash/SHA512/Internal.hs
--- a/lib/Crypto/Hash/SHA512/Internal.hs
+++ b/lib/Crypto/Hash/SHA512/Internal.hs
@@ -83,8 +83,44 @@
   --
   --   Runs in variable-time only for invalid inputs.
   (MAC a@(BI.PS _ _ la)) == (MAC b@(BI.PS _ _ lb))
-    | la /= lb  = False
-    | otherwise = BS.foldl' (B..|.) 0 (BS.packZipWith B.xor a b) == 0
+      | la /= lb   = False
+      | la == 64   =
+          -- fully-unrolled, fixed OR-tree over eight 64-bit lanes.
+          -- A standard 64-byte MAC folds through no loop-carried
+          -- accumulator, so there is no per-byte accumulator whose
+          -- nonzero span tracks the position of a differing byte:
+          -- the comparison time is independent of where (or whether)
+          -- the tags differ.
+          let !d0 = Exts.xor64# (word64le a 00) (word64le b 00)
+              !d1 = Exts.xor64# (word64le a 08) (word64le b 08)
+              !d2 = Exts.xor64# (word64le a 16) (word64le b 16)
+              !d3 = Exts.xor64# (word64le a 24) (word64le b 24)
+              !d4 = Exts.xor64# (word64le a 32) (word64le b 32)
+              !d5 = Exts.xor64# (word64le a 40) (word64le b 40)
+              !d6 = Exts.xor64# (word64le a 48) (word64le b 48)
+              !d7 = Exts.xor64# (word64le a 56) (word64le b 56)
+              !dl = (d0 `Exts.or64#` d1) `Exts.or64#`
+                    (d2 `Exts.or64#` d3)
+              !dh = (d4 `Exts.or64#` d5) `Exts.or64#`
+                    (d6 `Exts.or64#` d7)
+              !d  = dl `Exts.or64#` dh
+          in  Exts.isTrue# (Exts.eqWord64# d (Exts.wordToWord64# 0##))
+      | otherwise  = go 0 0
+    where
+      -- byte-serial fallback for nonstandard MAC lengths. 'hmac'
+      -- always yields 64 bytes, so this path is unreachable through
+      -- it; it exists only to keep the instance total for MACs built
+      -- directly via the exported constructor. The fused fold ORs the
+      -- bytewise XORs into an accumulator directly, rather than via
+      -- packZipWith, so no intermediate ByteString holding the
+      -- (secret-derived) difference bytes is ever materialised.
+      go :: Word8 -> Int -> Bool
+      go !acc !i
+        | i == la   = acc == 0
+        | otherwise =
+            let !x = BU.unsafeIndex a i
+                !y = BU.unsafeIndex b i
+            in  go (acc B..|. B.xor x y) (i + 1)
 
 -- | SHA512 block.
 newtype Block = Block
@@ -181,6 +217,41 @@
          s4 `Exts.or#` s5 `Exts.or#` s6 `Exts.or#` w7)
 {-# INLINE word64be #-}
 
+-- | Assemble the 64-bit word at the given byte offset, little-endian.
+--
+--   Byte order is immaterial to an equality test as long as both
+--   operands are assembled the same way; this is used only by the
+--   constant-time 'MAC' comparison. The length is not checked.
+word64le :: BS.ByteString -> Int -> Exts.Word64#
+word64le bs m =
+  let !(GHC.Word.W8# r0) = BU.unsafeIndex bs m
+      !(GHC.Word.W8# r1) = BU.unsafeIndex bs (m + 1)
+      !(GHC.Word.W8# r2) = BU.unsafeIndex bs (m + 2)
+      !(GHC.Word.W8# r3) = BU.unsafeIndex bs (m + 3)
+      !(GHC.Word.W8# r4) = BU.unsafeIndex bs (m + 4)
+      !(GHC.Word.W8# r5) = BU.unsafeIndex bs (m + 5)
+      !(GHC.Word.W8# r6) = BU.unsafeIndex bs (m + 6)
+      !(GHC.Word.W8# r7) = BU.unsafeIndex bs (m + 7)
+      !w0 = Exts.word8ToWord# r0
+      !w1 = Exts.word8ToWord# r1
+      !w2 = Exts.word8ToWord# r2
+      !w3 = Exts.word8ToWord# r3
+      !w4 = Exts.word8ToWord# r4
+      !w5 = Exts.word8ToWord# r5
+      !w6 = Exts.word8ToWord# r6
+      !w7 = Exts.word8ToWord# r7
+      !s1 = Exts.uncheckedShiftL# w1 8#
+      !s2 = Exts.uncheckedShiftL# w2 16#
+      !s3 = Exts.uncheckedShiftL# w3 24#
+      !s4 = Exts.uncheckedShiftL# w4 32#
+      !s5 = Exts.uncheckedShiftL# w5 40#
+      !s6 = Exts.uncheckedShiftL# w6 48#
+      !s7 = Exts.uncheckedShiftL# w7 56#
+  in  Exts.wordToWord64#
+        (w0 `Exts.or#` s1 `Exts.or#` s2 `Exts.or#` s3 `Exts.or#`
+         s4 `Exts.or#` s5 `Exts.or#` s6 `Exts.or#` s7)
+{-# INLINE word64le #-}
+
 -- parsing (final input) ------------------------------------------------------
 
 -- | Parse the final chunk of an input message, assuming it is less than
@@ -812,22 +883,14 @@
       !(GHC.Word.W8# b4) = BU.unsafeIndex dat 4
       !(GHC.Word.W8# b5) = BU.unsafeIndex dat 5
       !(GHC.Word.W8# b6) = BU.unsafeIndex dat 6
-      !w08 =
-            Exts.uncheckedShiftL# (Exts.word8ToWord# sep) 56#
-            `Exts.or#`
-            Exts.uncheckedShiftL# (Exts.word8ToWord# b0) 48#
-            `Exts.or#`
-            Exts.uncheckedShiftL# (Exts.word8ToWord# b1) 40#
-            `Exts.or#`
-            Exts.uncheckedShiftL# (Exts.word8ToWord# b2) 32#
-            `Exts.or#`
-            Exts.uncheckedShiftL# (Exts.word8ToWord# b3) 24#
-            `Exts.or#`
-            Exts.uncheckedShiftL# (Exts.word8ToWord# b4) 16#
-            `Exts.or#`
-            Exts.uncheckedShiftL# (Exts.word8ToWord# b5) 8#
-            `Exts.or#`
-            Exts.word8ToWord# b6
+      !w08 =       Exts.uncheckedShiftL# (Exts.word8ToWord# sep) 56#
+        `Exts.or#` Exts.uncheckedShiftL# (Exts.word8ToWord# b0)  48#
+        `Exts.or#` Exts.uncheckedShiftL# (Exts.word8ToWord# b1)  40#
+        `Exts.or#` Exts.uncheckedShiftL# (Exts.word8ToWord# b2)  32#
+        `Exts.or#` Exts.uncheckedShiftL# (Exts.word8ToWord# b3)  24#
+        `Exts.or#` Exts.uncheckedShiftL# (Exts.word8ToWord# b4)  16#
+        `Exts.or#` Exts.uncheckedShiftL# (Exts.word8ToWord# b5)  08#
+        `Exts.or#` Exts.word8ToWord# b6
   in  B v0 v1 v2 v3 v4 v5 v6 v7
         (Exts.wordToWord64# w08)
         (word64be dat 07) (word64be dat 15) (word64be dat 23)
diff --git a/ppad-sha512.cabal b/ppad-sha512.cabal
--- a/ppad-sha512.cabal
+++ b/ppad-sha512.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               ppad-sha512
-version:            0.2.2
+version:            0.2.3
 synopsis:           The SHA-512 and HMAC-SHA512 algorithms
 license:            MIT
 license-file:       LICENSE
