Repository: RetroAchievements/RAIntegration
Pull request #822 (2021-10-30 17:03:31)

several optimizations for search code
Improve both performance and memory footprint, particularly for consoles with larger amounts of exposed memory.
* break large ranges of matches into smaller groups when chunks of the original group are filtered out in a later group
* avoid comparing every item in a group if the entire group is unmodified
* use specialized functions for most common search types (i.e. "8-bit = Last Value")
* use bit vectors to keep track of matching items instead of an array of integers
* move several derived value calculations out of loops


Patch:
diff --git a/src/services/SearchResults.cpp b/src/services/SearchResults.cpp
index 1ea50283..dc189b0f 100644
--- a/src/services/SearchResults.cpp
+++ b/src/services/SearchResults.cpp
@@ -10,6 +10,10 @@
 
 #include <algorithm>
 
+// define this to use the generic filtering code for all search types
+// if defined, specialized templated code will be used for little endian searches
+#undef DISABLE_TEMPLATED_SEARCH
+
 namespace ra {
 namespace services {
 
@@ -49,23 +53,44 @@ public:
     // Gets the number of bytes after an address that are required to hold the data at the address
     virtual unsigned int GetPadding() const noexcept { return 0U; }
 
+    // Gets the number of bytes per virtual address
+    virtual unsigned int GetStride() const noexcept { return 1U; }
+
     // Gets the size of values handled by this implementation
     virtual MemSize GetMemSize() const noexcept { return MemSize::EightBit; }
 
-    // Determines if the specified address exists in the collection of addresses.
-    virtual bool ContainsAddress(const std::vector<ra::ByteAddress>& vAddresses, ra::ByteAddress nAddress) const
+    // Gets the number of addresses that are represented by the specified number of bytes
+    virtual unsigned int GetAddressCountForBytes(unsigned int nBytes) const noexcept
     {
-        return std::binary_search(vAddresses.begin(), vAddresses.end(), nAddress);
+        return nBytes - GetPadding();
     }
 
-    virtual bool IsAddressValid(_UNUSED ra::ByteAddress nAddress) const noexcept
+    // Gets the virtual address for a real address
+    virtual ra::ByteAddress ConvertFromRealAddress(ra::ByteAddress nAddress) const noexcept
     {
-        return true;
+        return nAddress;
     }
 
-    virtual size_t AdjustInitialAddressCount(size_t nBytes) const noexcept
+    // Gets the real address for a virtual address
+    virtual ra::ByteAddress ConvertToRealAddress(ra::ByteAddress nAddress) const noexcept
     {
-        return nBytes;
+        return nAddress;
+    }
+
+    // Determines if the specified real address exists in the collection of matched addresses.
+    virtual bool ContainsAddress(const SearchResults& srResults, ra::ByteAddress nAddress) const
+    {
+        if (nAddress & (GetStride() - 1))
+            return false;
+
+        nAddress = ConvertFromRealAddress(nAddress);
+        for (const auto& pBlock : srResults.m_vBlocks)
+        {
+            if (pBlock.ContainsAddress(nAddress))
+                return pBlock.ContainsMatchingAddress(nAddress);
+        }
+
+        return false;
     }
 
     // populates a vector of addresses that match the specified filter when applied to a previous search result
@@ -74,76 +99,108 @@ public:
         unsigned int nLargestBlock = 0U;
         for (auto& block : srPrevious.m_vBlocks)
         {
-            if (block.GetSize() > nLargestBlock)
-                nLargestBlock = block.GetSize();
+            if (block.GetBytesSize() > nLargestBlock)
+                nLargestBlock = block.GetBytesSize();
         }
 
         std::vector<unsigned char> vMemory(nLargestBlock);
         std::vector<ra::ByteAddress> vMatches;
         const auto& pEmulatorContext = ra::services::ServiceLocator::Get<ra::data::context::EmulatorContext>();
 
+        unsigned int nAdjustment = 0;
+        switch (srNew.GetFilterType())
+        {
+            case SearchFilterType::LastKnownValuePlus:
+                nAdjustment = srNew.GetFilterValue();
+                break;
+            case SearchFilterType::LastKnownValueMinus:
+                nAdjustment = ra::to_unsigned(-ra::to_signed(srNew.GetFilterValue()));
+                break;
+            default:
+                break;
+        }
+
         for (auto& block : srPrevious.m_vBlocks)
         {
-            pEmulatorContext.ReadMemory(block.GetAddress(), vMemory.data(), block.GetSize());
+            pEmulatorContext.ReadMemory(ConvertToRealAddress(block.GetFirstAddress()), vMemory.data(), block.GetBytesSize());
 
-            const auto nStop = block.GetSize() - GetPadding();
+            const auto nStop = block.GetBytesSize() - GetPadding();
 
             switch (srNew.GetFilterType())
             {
-                default:
                 case SearchFilterType::Constant:
-                    for (unsigned int i = 0; i < nStop; ++i)
-                    {
-                        const unsigned int nValue1 = BuildValue(&vMemory.at(i));
-                        const unsigned int nAddress = block.GetAddress() + i;
-                        ApplyFilter(vMatches, srPrevious, nAddress, srNew, nValue1, srNew.GetFilterValue());
-                    }
+                    ApplyConstantFilter(vMemory.data(), vMemory.data() + nStop, block,
+                        srNew.GetFilterComparison(), srNew.GetFilterValue(), vMatches);
                     break;
 
-                case SearchFilterType::InitialValue:
-                case SearchFilterType::LastKnownValue:
-                case SearchFilterType::LastKnownValuePlus:
-                case SearchFilterType::LastKnownValueMinus:
-                    for (unsigned int i = 0; i < nStop; ++i)
+                default:
+                    // if an entire block is unchanged, everything in it is equal to the LastKnownValue
+                    // or InitialValue and we don't have to check every address.
+                    if (memcmp(vMemory.data(), block.GetBytes(), block.GetBytesSize()) == 0)
                     {
-                        const unsigned int nValue1 = BuildValue(&vMemory.at(i));
-                        const unsigned int nValue2 = BuildValue(block.GetBytes() + i);
-                        const unsigned int nAddress = block.GetAddress() + i;
-                        ApplyFilter(vMatches, srPrevious, nAddress, srNew, nValue1, nValue2);
+                        switch (srNew.GetFilterComparison())
+                        {
+                            case ComparisonType::Equals:
+                            case ComparisonType::GreaterThanOrEqual:
+                            case ComparisonType::LessThanOrEqual:
+                                if (nAdjustment == 0)
+                                {
+                                    // entire block matches, copy the old block
+                                    MemBlock& newBlock = AddBlock(srNew, block.GetFirstAddress(), block.GetBytesSize(), block.GetMaxAddresses());
+                                    memcpy(newBlock.GetBytes(), block.GetBytes(), block.GetBytesSize());
+                                    newBlock.CopyMatchingAddresses(block);
+                                    continue;
+                                }
+                                else if (srNew.GetFilterComparison() == ComparisonType::Equals)
+                                {
+                                    // entire block matches, so adjustment won't match. discard the block
+                                    continue;
+                                }
+                                break;
+
+                            default:
+                                if (nAdjustment == 0)
+                                {
+                                    // entire block matches, discard it
+                                    continue;
+                                }
+                                break;
+                        }
+
+                        // have to check individual addresses to see if adjustment matches
                     }
+
+                    // per-address comparison
+                    ApplyCompareFilter(vMemory.data(), vMemory.data() + nStop, block,
+                        srNew.GetFilterComparison(), nAdjustment, vMatches);
                     break;
             }
 
             if (!vMatches.empty())
             {
-                AddBlock(srNew, vMatches, vMemory, block.GetAddress());
-                srNew.m_vMatchingAddresses.insert(srNew.m_vMatchingAddresses.end(), vMatches.begin(), vMatches.end());
+                AddBlocks(srNew, vMatches, vMemory, block.GetFirstAddress(), GetPadding());
                 vMatches.clear();
             }
         }
     }
 
     // gets the nIndex'th search result
-    virtual bool GetMatchingAddress(const SearchResults& srResults, gsl::index nIndex, _Out_ SearchResults::Result& result) const
+    bool GetMatchingAddress(const SearchResults& srResults, gsl::index nIndex, _Out_ SearchResults::Result& result) const noexcept
     {
         result.nSize = GetMemSize();
 
-        if (srResults.m_vMatchingAddresses.empty())
+        for (const auto& pBlock : srResults.m_vBlocks)
         {
-            if (!HasFirstAddress(srResults))
-                return false;
-
-            result.nAddress = GetFirstAddress(srResults) + gsl::narrow_cast<ra::ByteAddress>(nIndex);
-        }
-        else
-        {
-            if (ra::to_unsigned(nIndex) >= srResults.m_vMatchingAddresses.size())
-                return false;
+            if (nIndex < gsl::narrow_cast<gsl::index>(pBlock.GetMatchingAddressCount()))
+            {
+                result.nAddress = pBlock.GetMatchingAddress(nIndex);
+                return GetValue(pBlock, result);
+            }
 
-            result.nAddress = srResults.m_vMatchingAddresses.at(nIndex);
+            nIndex -= pBlock.GetMatchingAddressCount();
         }
 
-        return GetValue(srResults, result);
+        return false;
     }
 
     // gets the value associated with the address and size in the search result structure
@@ -166,7 +223,7 @@ public:
 
     virtual std::wstring GetFormattedValue(const SearchResults& pResults, ra::ByteAddress nAddress, MemSize nSize) const
     {
-        SearchResults::Result pResult{ nAddress, 0, nSize };
+        SearchResults::Result pResult{ ConvertFromRealAddress(nAddress), 0, nSize };
         if (GetValue(pResults, pResult))
             return GetFormattedValue(pResults, pResult);
 
@@ -182,18 +239,178 @@ public:
             *sFormattedValue = GetFormattedValue(pResults, pResult);
     }
 
-
 protected:
-    static const std::vector<ra::ByteAddress>& GetMatchingAddresses(const SearchResults& srResults) noexcept
+    // generic implementation for less used search types
+    virtual void ApplyConstantFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nConstantValue,
+        std::vector<ra::ByteAddress>& vMatches) const
     {
-        return srResults.m_vMatchingAddresses;
+        const auto nBlockAddress = pPreviousBlock.GetFirstAddress();
+        const auto nStride = GetStride();
+        const auto* pMatchingAddresses = pPreviousBlock.GetMatchingAddressPointer();
+
+        for (const auto* pScan = pBytes; pScan < pBytesStop; pScan += nStride)
+        {
+            const unsigned int nValue1 = BuildValue(pScan);
+            if (CompareValues(nValue1, nConstantValue, nComparison))
+            {
+                const ra::ByteAddress nAddress = nBlockAddress +
+                    ConvertFromRealAddress(gsl::narrow_cast<unsigned int>(pScan - pBytes));
+                if (pPreviousBlock.HasMatchingAddress(pMatchingAddresses, nAddress))
+                    vMatches.push_back(nAddress);
+            }
+        }
     }
 
-    static std::vector<ra::ByteAddress>& GetMatchingAddresses(SearchResults& srResults) noexcept
+    // generic implementation for less used search types
+    virtual void ApplyCompareFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        std::vector<ra::ByteAddress>& vMatches) const
     {
-        return srResults.m_vMatchingAddresses;
+        const auto* pBlockBytes = pPreviousBlock.GetBytes();
+        const auto nBlockAddress = pPreviousBlock.GetFirstAddress();
+        const auto nStride = GetStride();
+        const auto* pMatchingAddresses = pPreviousBlock.GetMatchingAddressPointer();
+
+        for (const auto* pScan = pBytes; pScan < pBytesStop; pScan += nStride, pBlockBytes += nStride)
+        {
+            const unsigned int nValue1 = BuildValue(pScan);
+            const unsigned int nValue2 = BuildValue(pBlockBytes) + nAdjustment;
+            if (CompareValues(nValue1, nValue2, nComparison))
+            {
+                const ra::ByteAddress nAddress = nBlockAddress +
+                    ConvertFromRealAddress(gsl::narrow_cast<unsigned int>(pScan - pBytes));
+                if (pPreviousBlock.HasMatchingAddress(pMatchingAddresses, nAddress))
+                    vMatches.push_back(nAddress);
+            }
+        }
     }
 
+    // templated implementation for commonly used search types for best performance
+#ifndef DISABLE_TEMPLATED_SEARCH
+ #pragma warning(push)
+ #pragma warning(disable : 5045)
+    template<typename TSize, bool TIsConstantFilter, int TStride, ComparisonType TComparison>
+    void ApplyCompareFilterLittleEndian(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, unsigned nAdjustment,
+        std::vector<ra::ByteAddress>& vMatches) const
+    {
+        const auto* pScan = pBytes;
+        if (pScan == nullptr)
+            return;
+
+        ra::ByteAddress nAddress = pPreviousBlock.GetFirstAddress();
+        constexpr int TBlockStride = TIsConstantFilter ? 0 : TStride;
+        const auto* pBlockBytes = TIsConstantFilter ? pScan : pPreviousBlock.GetBytes();
+        Expects(pBlockBytes != nullptr);
+
+        const auto* pMatchingAddresses = pPreviousBlock.GetMatchingAddressPointer();
+        if (!pMatchingAddresses)
+        {
+            // all addresses in previous block match
+            for (; pScan < pBytesStop; pScan += TStride, pBlockBytes += TBlockStride)
+            {
+                GSL_SUPPRESS_TYPE1
+                {
+                    const unsigned int nValue1 = *(reinterpret_cast<const TSize*>(pScan));
+                    const unsigned int nValue2 = TIsConstantFilter ? nAdjustment :
+                        *(reinterpret_cast<const TSize*>(pBlockBytes)) + nAdjustment;
+
+                    if (CompareValues(nValue1, nValue2, TComparison))
+                        vMatches.push_back(nAddress);
+                }
+
+                ++nAddress;
+            }
+        }
+        else
+        {
+            // only a subset of addresses in the previous block match
+            uint8_t nMask = 0x01;
+            for (; pScan < pBytesStop; pScan += TStride, pBlockBytes += TBlockStride)
+            {
+                const bool bPreviousMatch = *pMatchingAddresses & nMask;
+                if (nMask == 0x80)
+                {
+                    nMask = 0x01;
+                    ++pMatchingAddresses;
+                }
+                else
+                {
+                    nMask <<= 1;
+                }
+
+                if (bPreviousMatch)
+                {
+                    GSL_SUPPRESS_TYPE1
+                    {
+                        const unsigned int nValue1 = *(reinterpret_cast<const TSize*>(pScan));
+                        const unsigned int nValue2 = TIsConstantFilter ? nAdjustment :
+                            *(reinterpret_cast<const TSize*>(pBlockBytes)) + nAdjustment;
+
+                        if (CompareValues(nValue1, nValue2, TComparison))
+                            vMatches.push_back(nAddress);
+                    }
+                }
+
+                ++nAddress;
+            }
+        }
+    }
+
+    /// <summary>
+    /// Finds items in a block of memory that match a specified filter.
+    /// </summary>
+    /// <typeparam name="TSize">The size of each item being compared</typeparam>
+    /// <typeparam name="TIsConstantFilter"><c>true</c> if nAdjustment is a constant.</typeparam>
+    /// <typeparam name="TStride">The amount to advance the pointer after each comparison</typeparam>
+    /// <param name="pBytes">The memory to examine</param>
+    /// <param name="pBytesStop">When to stop examining memory</param>
+    /// <param name="pPreviousBlock">The block being compared against</param>
+    /// <param name="nComparison">The comparison to perform</param>
+    /// <param name="nAdjustment">The adjustment to apply to each value before comparing, or the constant to compare against</param>
+    /// <param name="vMatches">[out] The list of matching addresses</param>
+    template<typename TSize, bool TIsConstantFilter, int TStride = 1>
+    void ApplyCompareFilterLittleEndian(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        std::vector<ra::ByteAddress>& vMatches) const
+    {
+        switch (nComparison)
+        {
+            case ComparisonType::Equals:
+                return ApplyCompareFilterLittleEndian<TSize, TIsConstantFilter, TStride, ComparisonType::Equals>
+                    (pBytes, pBytesStop, pPreviousBlock, nAdjustment, vMatches);
+            case ComparisonType::LessThan:
+                return ApplyCompareFilterLittleEndian<TSize, TIsConstantFilter, TStride, ComparisonType::LessThan>
+                    (pBytes, pBytesStop, pPreviousBlock, nAdjustment, vMatches);
+            case ComparisonType::LessThanOrEqual:
+                return ApplyCompareFilterLittleEndian<TSize, TIsConstantFilter, TStride, ComparisonType::LessThanOrEqual>
+                    (pBytes, pBytesStop, pPreviousBlock, nAdjustment, vMatches);
+            case ComparisonType::GreaterThan:
+                return ApplyCompareFilterLittleEndian<TSize, TIsConstantFilter, TStride, ComparisonType::GreaterThan>
+                    (pBytes, pBytesStop, pPreviousBlock, nAdjustment, vMatches);
+            case ComparisonType::GreaterThanOrEqual:
+                return ApplyCompareFilterLittleEndian<TSize, TIsConstantFilter, TStride, ComparisonType::GreaterThanOrEqual>
+                    (pBytes, pBytesStop, pPreviousBlock, nAdjustment, vMatches);
+            case ComparisonType::NotEqualTo:
+                return ApplyCompareFilterLittleEndian<TSize, TIsConstantFilter, TStride, ComparisonType::NotEqualTo>
+                    (pBytes, pBytesStop, pPreviousBlock, nAdjustment, vMatches);
+        }
+    }
+ #pragma warning(pop)
+#else // #ifndef DISABLE_TEMPLATED_SEARCH
+    template<typename TSize, bool TIsConstantFilter, int TStride = 1>
+    void ApplyCompareFilterLittleEndian(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        std::vector<ra::ByteAddress>& vMatches) const
+    {
+        if (TIsConstantFilter)
+            SearchImpl::ApplyConstantFilter(pBytes, pBytesStop, pPreviousBlock, nComparison, nAdjustment, vMatches);
+        else
+            SearchImpl::ApplyCompareFilter(pBytes, pBytesStop, pPreviousBlock, nComparison, nAdjustment, vMatches);
+    }
+#endif
+
     static bool HasFirstAddress(const SearchResults& srResults) noexcept
     {
         return !srResults.m_vBlocks.empty();
@@ -201,7 +418,7 @@ protected:
 
     static ra::ByteAddress GetFirstAddress(const SearchResults& srResults) noexcept
     {
-        return srResults.m_vBlocks.front().GetAddress();
+        return srResults.m_vBlocks.front().GetFirstAddress();
     }
 
     static const std::vector<impl::MemBlock>& GetBlocks(const SearchResults& srResults) noexcept
@@ -209,13 +426,14 @@ protected:
         return srResults.m_vBlocks;
     }
 
+    // Gets a value from a memory block using a virtual address (in result.nAddress)
     virtual bool GetValue(const impl::MemBlock& block, SearchResults::Result& result) const noexcept
     {
-        if (result.nAddress < block.GetAddress())
+        if (result.nAddress < block.GetFirstAddress())
             return false;
 
-        const unsigned int nOffset = result.nAddress - block.GetAddress();
-        if (nOffset >= block.GetSize() - GetPadding())
+        const unsigned int nOffset = result.nAddress - block.GetFirstAddress();
+        if (nOffset >= block.GetBytesSize() - GetPadding())
             return false;
 
         result.nValue = BuildValue(block.GetBytes() + nOffset);
@@ -228,48 +446,46 @@ protected:
         return ptr[0];
     }
 
-    virtual void ApplyFilter(std::vector<ra::ByteAddress>& vMatches, const SearchResults& srPrevious,
-        ra::ByteAddress nAddress, const SearchResults& srFilter,
-        unsigned int nValue1, unsigned int nValue2) const
+    static impl::MemBlock& AddBlock(SearchResults& srResults, ra::ByteAddress nAddress, unsigned int nSize, unsigned int nMaxAddresses)
     {
-        switch (srFilter.GetFilterType())
+        return srResults.m_vBlocks.emplace_back(nAddress, nSize, nMaxAddresses);
+    }
+
+    void AddBlocks(SearchResults& srNew, std::vector<ra::ByteAddress>& vMatches,
+        std::vector<unsigned char>& vMemory, ra::ByteAddress nPreviousBlockFirstAddress, unsigned int nPadding) const
+    {
+        const gsl::index nStopIndex = gsl::narrow_cast<gsl::index>(vMatches.size()) - 1;
+        gsl::index nFirstIndex = 0;
+        gsl::index nLastIndex = 0;
+        do
         {
-            case SearchFilterType::LastKnownValuePlus:
-                nValue2 += srFilter.GetFilterValue();
-                break;
+            const auto nFirstMatchingAddress = vMatches.at(nFirstIndex);
+            while (nLastIndex < nStopIndex && vMatches.at(nLastIndex + 1) - nFirstMatchingAddress < 64)
+                nLastIndex++;
 
-            case SearchFilterType::LastKnownValueMinus:
-                nValue2 -= srFilter.GetFilterValue();
-                break;
+            // determine how many bytes we need to capture
+            const auto nFirstRealAddress = ConvertToRealAddress(nFirstMatchingAddress);
+            const auto nLastRealAddress = ConvertToRealAddress(vMatches.at(nLastIndex));
+            const unsigned int nBlockSize = nLastRealAddress - nFirstRealAddress + 1 + nPadding;
 
-            default:
-                break;
-        }
+            // determine the maximum number of addresses that can be associated to the captured bytes
+            const auto nMaxAddresses = GetAddressCountForBytes(nBlockSize);
 
-        if (CompareValues(nValue1, nValue2, srFilter.GetFilterComparison()))
-            AddMatch(vMatches, srPrevious, nAddress);
-    }
+            // determine the address of the first captured byte
+            const auto nFirstAddress = ConvertFromRealAddress(nFirstRealAddress);
 
-    static void AddMatch(std::vector<ra::ByteAddress>& vMatches, const SearchResults& srPrevious, ra::ByteAddress nAddress)
-    {
-        if (srPrevious.m_nFilterType == SearchFilterType::None ||
-            std::binary_search(srPrevious.m_vMatchingAddresses.begin(), srPrevious.m_vMatchingAddresses.end(), nAddress))
-        {
-            vMatches.push_back(nAddress);
-        }
-    }
+            // allocate the new block
+            MemBlock& block = AddBlock(srNew, nFirstAddress, nBlockSize, nMaxAddresses);
 
-    static impl::MemBlock& AddBlock(SearchResults& srNew, ra::ByteAddress nAddress, unsigned int nSize)
-    {
-        return srNew.m_vBlocks.emplace_back(nAddress, nSize);
-    }
+            // capture the subset of data that corresponds to the subset of matches
+            const auto nOffset = nFirstRealAddress - ConvertToRealAddress(nPreviousBlockFirstAddress);
+            memcpy(block.GetBytes(), &vMemory.at(nOffset), nBlockSize);
 
-    virtual void AddBlock(SearchResults& srNew, std::vector<ra::ByteAddress>& vMatches,
-        std::vector<unsigned char>& vMemory, ra::ByteAddress nFirstMemoryAddress) const
-    {
-        const unsigned int nBlockSize = vMatches.back() - vMatches.front() + GetPadding() + 1;
-        MemBlock& block = AddBlock(srNew, vMatches.front(), nBlockSize);
-        memcpy(block.GetBytes(), &vMemory.at(vMatches.front() - nFirstMemoryAddress), nBlockSize);
+            // capture the matched addresses
+            block.SetMatchingAddresses(vMatches, nFirstIndex, nLastIndex);
+
+            nFirstIndex = nLastIndex + 1;
+        } while (nFirstIndex < gsl::narrow_cast<gsl::index>(vMatches.size()));
     }
 };
 
@@ -277,98 +493,108 @@ class FourBitSearchImpl : public SearchImpl
 {
     MemSize GetMemSize() const noexcept override { return MemSize::Nibble_Lower; }
 
-    bool ContainsAddress(const std::vector<ra::ByteAddress>& vAddresses, ra::ByteAddress nAddress) const override
+    unsigned int GetAddressCountForBytes(unsigned int nBytes) const noexcept override
     {
-        nAddress <<= 1;
-        const auto iter = std::lower_bound(vAddresses.begin(), vAddresses.end(), nAddress);
-        if (iter == vAddresses.end())
-            return false;
+        return nBytes * 2;
+    }
 
-        return (*iter == nAddress || *iter == (nAddress | 1));
+    ra::ByteAddress ConvertFromRealAddress(ra::ByteAddress nAddress) const noexcept override
+    {
+        return nAddress * 2;
     }
 
-    size_t AdjustInitialAddressCount(size_t nCount) const noexcept override
+    ra::ByteAddress ConvertToRealAddress(ra::ByteAddress nAddress) const noexcept override
     {
-        return nCount * 2;
+        return nAddress / 2;
     }
 
-    void ApplyFilter(std::vector<ra::ByteAddress>& vMatches, const SearchResults& srPrevious,
-        ra::ByteAddress nAddress, const SearchResults& srFilter,
-        unsigned int nValue1, unsigned int nValue2) const override
+    bool ContainsAddress(const SearchResults& srResults, ra::ByteAddress nAddress) const override
     {
-        const unsigned int nValue1a = (nValue1 & 0x0F);
-        const unsigned int nValue1b = ((nValue1 >> 4) & 0x0F);
-        unsigned int nValue2a = (nValue2 & 0x0F);
-        unsigned int nValue2b = ((nValue2 >> 4) & 0x0F);
+        nAddress <<= 1;
 
-        switch (srFilter.GetFilterType())
+        for (const auto& pBlock : GetBlocks(srResults))
         {
-            case SearchFilterType::LastKnownValuePlus:
-                nValue2a += srFilter.GetFilterValue();
-                nValue2b += srFilter.GetFilterValue();
-                break;
-
-            case SearchFilterType::LastKnownValueMinus:
-                nValue2a -= srFilter.GetFilterValue();
-                nValue2b -= srFilter.GetFilterValue();
-                break;
-
-            case SearchFilterType::Constant:
-                nValue2b = nValue2a;
-                break;
-
-            default:
-                break;
+            if (pBlock.ContainsAddress(nAddress))
+                return pBlock.ContainsMatchingAddress(nAddress) || pBlock.ContainsMatchingAddress(nAddress | 1);
         }
 
-        if (CompareValues(nValue1a, nValue2a, srFilter.GetFilterComparison()))
-            AddMatch(vMatches, srPrevious, nAddress << 1);
-        if (CompareValues(nValue1b, nValue2b, srFilter.GetFilterComparison()))
-            AddMatch(vMatches, srPrevious, (nAddress << 1) | 1);
+        return false;
     }
 
-    bool GetMatchingAddress(const SearchResults& srResults, gsl::index nIndex, _Out_ SearchResults::Result& result) const override
+    void ApplyConstantFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nConstantValue,
+        std::vector<ra::ByteAddress>& vMatches) const override
     {
-        result.nSize = GetMemSize();
-
-        const auto& vMatchingAddresses = GetMatchingAddresses(srResults);
-        if (vMatchingAddresses.empty())
+        const auto nBlockAddress = pPreviousBlock.GetFirstAddress();
+        const auto* pMatchingAddresses = pPreviousBlock.GetMatchingAddressPointer();
+        for (const auto* pScan = pBytes; pScan < pBytesStop; ++pScan)
         {
-            if (!HasFirstAddress(srResults))
-                return false;
+            const unsigned int nValue1 = BuildValue(pScan);
+            const unsigned int nValue1a = (nValue1 & 0x0F);
+            const unsigned int nValue1b = ((nValue1 >> 4) & 0x0F);
 
-            result.nAddress = (GetFirstAddress(srResults) << 1) + gsl::narrow_cast<ra::ByteAddress>(nIndex);
-        }
-        else
-        {
-            if (ra::to_unsigned(nIndex) >= vMatchingAddresses.size())
-                return false;
+            if (CompareValues(nValue1a, nConstantValue, nComparison))
+            {
+                const unsigned int nAddress = nBlockAddress + (gsl::narrow_cast<unsigned>(pScan - pBytes) << 1);
+                if (pPreviousBlock.HasMatchingAddress(pMatchingAddresses, nAddress))
+                    vMatches.push_back(nAddress);
+            }
 
-            result.nAddress = vMatchingAddresses.at(nIndex);
+            if (CompareValues(nValue1b, nConstantValue, nComparison))
+            {
+                const unsigned int nAddress = (nBlockAddress + (gsl::narrow_cast<unsigned>(pScan - pBytes) << 1)) | 1;
+                if (pPreviousBlock.HasMatchingAddress(pMatchingAddresses, nAddress))
+                    vMatches.push_back(nAddress);
+            }
         }
-
-        if (result.nAddress & 0x01)
-            result.nSize = MemSize::Nibble_Upper;
-
-        result.nAddress >>= 1;
-        return SearchImpl::GetValue(srResults, result);
     }
 
-protected:
-    void AddBlock(SearchResults& srNew, std::vector<ra::ByteAddress>& vMatches,
-        std::vector<unsigned char>& vMemory, ra::ByteAddress nFirstMemoryAddress) const override
+    void ApplyCompareFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        std::vector<ra::ByteAddress>& vMatches) const override
     {
-        const ra::ByteAddress nFirstAddress = (vMatches.front() >> 1);
-        const unsigned int nBlockSize = (vMatches.back() >> 1) - nFirstAddress + 1;
-        MemBlock& block = SearchImpl::AddBlock(srNew, nFirstAddress, nBlockSize);
-        memcpy(block.GetBytes(), &vMemory.at(nFirstAddress - nFirstMemoryAddress), nBlockSize);
+        const auto* pBlockBytes = pPreviousBlock.GetBytes();
+        const auto nBlockAddress = pPreviousBlock.GetFirstAddress();
+        const auto* pMatchingAddresses = pPreviousBlock.GetMatchingAddressPointer();
+        for (const auto* pScan = pBytes; pScan < pBytesStop; ++pScan)
+        {
+            const unsigned int nValue1 = BuildValue(pScan);
+            const unsigned int nValue1a = (nValue1 & 0x0F);
+            const unsigned int nValue1b = ((nValue1 >> 4) & 0x0F);
+            const unsigned int nValue2 = BuildValue(pBlockBytes++);
+            const unsigned int nValue2a = (nValue2 & 0x0F) + nAdjustment;
+            const unsigned int nValue2b = ((nValue2 >> 4) & 0x0F) + nAdjustment;
+
+            if (CompareValues(nValue1a, nValue2a, nComparison))
+            {
+                const unsigned int nAddress = nBlockAddress + (gsl::narrow_cast<unsigned>(pScan - pBytes) << 1);
+                if (pPreviousBlock.HasMatchingAddress(pMatchingAddresses, nAddress))
+                    vMatches.push_back(nAddress);
+            }
+
+            if (CompareValues(nValue1b, nValue2b, nComparison))
+            {
+                const unsigned int nAddress = nBlockAddress + (gsl::narrow_cast<unsigned>(pScan - pBytes) << 1) | 1;
+                if (pPreviousBlock.HasMatchingAddress(pMatchingAddresses, nAddress))
+                    vMatches.push_back(nAddress);
+            }
+        }
     }
 
+protected:
     bool GetValue(const impl::MemBlock& block, SearchResults::Result& result) const noexcept override
     {
-        if (!SearchImpl::GetValue(block, result))
+        if (result.nAddress & 1)
+            result.nSize = MemSize::Nibble_Upper;
+
+        result.nAddress >>= 1;
+
+        const unsigned int nOffset = result.nAddress - (block.GetFirstAddress() >> 1);
+        if (nOffset >= block.GetBytesSize())
             return false;
 
+        result.nValue = BuildValue(block.GetBytes() + nOffset);
+
         if (result.nSize == MemSize::Nibble_Lower)
             result.nValue &= 0x0F;
         else
@@ -380,7 +606,22 @@ protected:
 
 class EightBitSearchImpl : public SearchImpl
 {
+protected:
+    void ApplyConstantFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        std::vector<ra::ByteAddress>& vMatches) const override
+    {
+        ApplyCompareFilterLittleEndian<uint8_t, true>(pBytes, pBytesStop,
+            pPreviousBlock, nComparison, nAdjustment, vMatches);
+    }
 
+    void ApplyCompareFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        std::vector<ra::ByteAddress>& vMatches) const override
+    {
+        ApplyCompareFilterLittleEndian<uint8_t, false>(pBytes, pBytesStop,
+            pPreviousBlock, nComparison, nAdjustment, vMatches);
+    }
 };
 
 class SixteenBitSearchImpl : public SearchImpl
@@ -395,6 +636,24 @@ public:
         GSL_SUPPRESS_F6 Expects(ptr != nullptr);
         return (ptr[1] << 8) | ptr[0];
     }
+
+protected:
+    void ApplyConstantFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        std::vector<ra::ByteAddress>& vMatches) const override
+    {
+        ApplyCompareFilterLittleEndian<uint16_t, true>(pBytes, pBytesStop,
+            pPreviousBlock, nComparison, nAdjustment, vMatches);
+    }
+
+    void ApplyCompareFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        std::vector<ra::ByteAddress>& vMatches) const override
+    {
+        ApplyCompareFilterLittleEndian<uint16_t, false>(pBytes, pBytesStop,
+            pPreviousBlock, nComparison, nAdjustment, vMatches);
+    }
+
 };
 
 class ThirtyTwoBitSearchImpl : public SearchImpl
@@ -409,87 +668,121 @@ public:
         GSL_SUPPRESS_F6 Expects(ptr != nullptr);
         return (ptr[3] << 24) | (ptr[2] << 16) | (ptr[1] << 8) | ptr[0];
     }
+
+protected:
+    void ApplyConstantFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        std::vector<ra::ByteAddress>& vMatches) const override
+    {
+        ApplyCompareFilterLittleEndian<uint32_t, true>(pBytes, pBytesStop,
+            pPreviousBlock, nComparison, nAdjustment, vMatches);
+    }
+
+    void ApplyCompareFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        std::vector<ra::ByteAddress>& vMatches) const override
+    {
+        ApplyCompareFilterLittleEndian<uint32_t, false>(pBytes, pBytesStop,
+            pPreviousBlock, nComparison, nAdjustment, vMatches);
+    }
+
 };
 
 class ThirtyTwoBitAlignedSearchImpl : public ThirtyTwoBitSearchImpl
 {
 public:
-    bool IsAddressValid(ra::ByteAddress nAddress) const noexcept override
+    unsigned int GetStride() const noexcept override { return 4; }
+
+    unsigned int GetAddressCountForBytes(unsigned int nBytes) const noexcept override
     {
-        return ((nAddress & 3) == 0);
+        return nBytes / 4;
     }
 
-    size_t AdjustInitialAddressCount(size_t nCount) const noexcept override
+    ra::ByteAddress ConvertFromRealAddress(ra::ByteAddress nAddress) const noexcept override
     {
-        return (nCount + GetPadding()) / 4; 
+        return nAddress / 4;
     }
 
-    bool GetMatchingAddress(const SearchResults& srResults, gsl::index nIndex, _Out_ SearchResults::Result& result) const override
+    ra::ByteAddress ConvertToRealAddress(ra::ByteAddress nAddress) const noexcept override
     {
-        result.nSize = GetMemSize();
-
-        if (srResults.GetFilterType() == SearchFilterType::None)
-        {
-            if (!HasFirstAddress(srResults))
-                return false;
+        return nAddress * 4;
+    }
 
-            result.nAddress = ((GetFirstAddress(srResults) + 3) & ~3) + gsl::narrow_cast<ra::ByteAddress>(nIndex * 4);
-            return GetValue(srResults, result);
-        }
+protected:
+    void ApplyConstantFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        std::vector<ra::ByteAddress>& vMatches) const override
+    {
+        ApplyCompareFilterLittleEndian<uint32_t, true, 4>(pBytes, pBytesStop,
+            pPreviousBlock, nComparison, nAdjustment, vMatches);
+    }
 
-        return ThirtyTwoBitSearchImpl::GetMatchingAddress(srResults, nIndex, result);
+    void ApplyCompareFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        std::vector<ra::ByteAddress>& vMatches) const override
+    {
+        ApplyCompareFilterLittleEndian<uint32_t, false, 4>(pBytes, pBytesStop,
+            pPreviousBlock, nComparison, nAdjustment, vMatches);
     }
 
-    void ApplyFilter(std::vector<ra::ByteAddress>& vMatches, const SearchResults& srPrevious,
-        ra::ByteAddress nAddress, const SearchResults& srFilter,
-        unsigned int nValue1, unsigned int nValue2) const override
+
+    bool GetValue(const impl::MemBlock& block, SearchResults::Result& result) const noexcept override
     {
-        if (IsAddressValid(nAddress))
-        {
-            ThirtyTwoBitSearchImpl::ApplyFilter(vMatches, srPrevious, nAddress,
-                srFilter, nValue1, nValue2);
-        }
+        result.nAddress *= 4;
+
+        const unsigned int nOffset = result.nAddress - (block.GetFirstAddress() * 4);
+        result.nValue = BuildValue(block.GetBytes() + nOffset);
+
+        return true;
     }
 };
 
 class SixteenBitAlignedSearchImpl : public SixteenBitSearchImpl
 {
 public:
-    bool IsAddressValid(ra::ByteAddress nAddress) const noexcept override
+    unsigned int GetStride() const noexcept override { return 2; }
+
+    unsigned int GetAddressCountForBytes(unsigned int nBytes) const noexcept override
     {
-        return ((nAddress & 1) == 0);
+        return nBytes / 2;
     }
 
-    size_t AdjustInitialAddressCount(size_t nCount) const noexcept override
+    ra::ByteAddress ConvertFromRealAddress(ra::ByteAddress nAddress) const noexcept override
     {
-        return (nCount + GetPadding()) / 2;
+        return nAddress / 2;
     }
 
-    bool GetMatchingAddress(const SearchResults& srResults, gsl::index nIndex, _Out_ SearchResults::Result& result) const override
+    ra::ByteAddress ConvertToRealAddress(ra::ByteAddress nAddress) const noexcept override
     {
-        result.nSize = GetMemSize();
-
-        if (srResults.GetFilterType() == SearchFilterType::None)
-        {
-            if (!HasFirstAddress(srResults))
-                return false;
+        return nAddress * 2;
+    }
 
-            result.nAddress = ((GetFirstAddress(srResults) + 1) & ~1) + gsl::narrow_cast<ra::ByteAddress>(nIndex * 2);
-            return GetValue(srResults, result);
-        }
+protected:
+    void ApplyConstantFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        std::vector<ra::ByteAddress>& vMatches) const override
+    {
+        ApplyCompareFilterLittleEndian<uint16_t, true, 2>(pBytes, pBytesStop,
+            pPreviousBlock, nComparison, nAdjustment, vMatches);
+    }
 
-        return SixteenBitSearchImpl::GetMatchingAddress(srResults, nIndex, result);
+    void ApplyCompareFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        std::vector<ra::ByteAddress>& vMatches) const override
+    {
+        ApplyCompareFilterLittleEndian<uint16_t, false, 2>(pBytes, pBytesStop,
+            pPreviousBlock, nComparison, nAdjustment, vMatches);
     }
 
-    void ApplyFilter(std::vector<ra::ByteAddress>& vMatches, const SearchResults& srPrevious,
-        ra::ByteAddress nAddress, const SearchResults& srFilter,
-        unsigned int nValue1, unsigned int nValue2) const override
+
+    bool GetValue(const impl::MemBlock& block, SearchResults::Result& result) const noexcept override
     {
-        if (IsAddressValid(nAddress))
-        {
-            SixteenBitSearchImpl::ApplyFilter(vMatches, srPrevious, nAddress,
-                srFilter, nValue1, nValue2);
-        }
+        result.nAddress *= 2;
+
+        const unsigned int nOffset = result.nAddress - (block.GetFirstAddress() * 2);
+        result.nValue = BuildValue(block.GetBytes() + nOffset);
+
+        return true;
     }
 };
 
@@ -542,8 +835,8 @@ public:
         unsigned int nLargestBlock = 0U;
         for (auto& block : GetBlocks(srPrevious))
         {
-            if (block.GetSize() > nLargestBlock)
-                nLargestBlock = block.GetSize();
+            if (block.GetBytesSize() > nLargestBlock)
+                nLargestBlock = block.GetBytesSize();
         }
 
         std::vector<unsigned char> vMemory(nLargestBlock);
@@ -552,9 +845,11 @@ public:
 
         for (auto& block : GetBlocks(srPrevious))
         {
-            pEmulatorContext.ReadMemory(block.GetAddress(), vMemory.data(), block.GetSize());
+            pEmulatorContext.ReadMemory(block.GetFirstAddress(), vMemory.data(), block.GetBytesSize());
 
-            const auto nStop = block.GetSize() - 1;
+            const auto nStop = block.GetBytesSize() - 1;
+            const auto nFilterComparison = srNew.GetFilterComparison();
+            const auto* pMatchingAddresses = block.GetMatchingAddressPointer();
 
             switch (srNew.GetFilterType())
             {
@@ -562,10 +857,11 @@ public:
                 case SearchFilterType::Constant:
                     for (unsigned int i = 0; i < nStop; ++i)
                     {
-                        if (CompareMemory(&vMemory.at(i), &vSearchText.at(0), nCompareLength, srNew.GetFilterComparison()))
+                        if (CompareMemory(&vMemory.at(i), &vSearchText.at(0), nCompareLength, nFilterComparison))
                         {
-                            const unsigned int nAddress = block.GetAddress() + i;
-                            AddMatch(vMatches, srPrevious, nAddress);
+                            const unsigned int nAddress = block.GetFirstAddress() + i;
+                            if (block.HasMatchingAddress(pMatchingAddresses, nAddress))
+                                vMatches.push_back(nAddress);
                         }
                     }
                     break;
@@ -574,10 +870,11 @@ public:
                 case SearchFilterType::LastKnownValue:
                     for (unsigned int i = 0; i < nStop; ++i)
                     {
-                        if (CompareMemory(&vMemory.at(i), block.GetBytes() + i, nCompareLength, srNew.GetFilterComparison()))
+                        if (CompareMemory(&vMemory.at(i), block.GetBytes() + i, nCompareLength, nFilterComparison))
                         {
-                            const unsigned int nAddress = block.GetAddress() + i;
-                            AddMatch(vMatches, srPrevious, nAddress);
+                            const unsigned int nAddress = block.GetFirstAddress() + i;
+                            if (block.HasMatchingAddress(pMatchingAddresses, nAddress))
+                                vMatches.push_back(nAddress);
                         }
                     }
                     break;
@@ -585,16 +882,9 @@ public:
 
             if (!vMatches.empty())
             {
-                auto& vMatchingAddresses = GetMatchingAddresses(srNew);
-                vMatchingAddresses.insert(vMatchingAddresses.end(), vMatches.begin(), vMatches.end());
-
                 // adjust the block size to account for the length of the string to ensure
                 // the block contains the whole string
-                // (duplicates the AddBlock function, but replaces "GetPadding()-1" with "nCompareLength")
-                const size_t nBlockSize = vMatches.back() - vMatches.front() + nCompareLength;
-                MemBlock& newBlock = AddBlock(srNew, vMatches.front(), gsl::narrow_cast<unsigned int>(nBlockSize));
-                memcpy(newBlock.GetBytes(), &vMemory.at(vMatches.front() - block.GetAddress()), nBlockSize);
-
+                AddBlocks(srNew, vMatches, vMemory, block.GetFirstAddress(), gsl::narrow_cast<unsigned int>(nCompareLength - 1));
                 vMatches.clear();
             }
         }
@@ -693,6 +983,147 @@ static SixteenBitBigEndianSearchImpl s_pSixteenBitBigEndianSearchImpl;
 static ThirtyTwoBitBigEndianSearchImpl s_pThirtyTwoBitBigEndianSearchImpl;
 static AsciiTextSearchImpl s_pAsciiTextSearchImpl;
 
+uint8_t* MemBlock::AllocateMatchingAddresses() noexcept
+{
+    const auto nAddressesSize = (m_nMaxAddresses + 7) / 8;
+    if (nAddressesSize > sizeof(m_vAddresses))
+    {
+        if (m_pAddresses == nullptr)
+            m_pAddresses = new (std::nothrow) uint8_t[nAddressesSize];
+
+        return m_pAddresses;
+    }
+
+    return &m_vAddresses[0];
+}
+
+void MemBlock::SetMatchingAddresses(std::vector<ra::ByteAddress>& vAddresses, gsl::index nFirstIndex, gsl::index nLastIndex)
+{
+    m_nMatchingAddresses = gsl::narrow_cast<unsigned int>(nLastIndex - nFirstIndex) + 1;
+
+    if (m_nMatchingAddresses != m_nMaxAddresses)
+    {
+        const auto nAddressesSize = (m_nMaxAddresses + 7) / 8;
+        unsigned char* pAddresses = AllocateMatchingAddresses();
+        Expects(pAddresses != nullptr);
+
+        memset(pAddresses, 0, nAddressesSize);
+        for (gsl::index nIndex = nFirstIndex; nIndex <= nLastIndex; ++nIndex)
+        {
+            const auto nOffset = vAddresses.at(nIndex) - m_nFirstAddress;
+            const auto nBit = 1 << (nOffset & 7);
+            pAddresses[nOffset >> 3] |= nBit;
+        }
+    }
+}
+
+void MemBlock::CopyMatchingAddresses(const MemBlock& pSource)
+{
+    Expects(pSource.m_nMaxAddresses == m_nMaxAddresses);
+    if (pSource.AreAllAddressesMatching())
+    {
+        m_nMatchingAddresses = m_nMaxAddresses;
+    }
+    else
+    {
+        const auto nAddressesSize = (m_nMaxAddresses + 7) / 8;
+        unsigned char* pAddresses = AllocateMatchingAddresses();
+        memcpy(pAddresses, pSource.GetMatchingAddressPointer(), nAddressesSize);
+        m_nMatchingAddresses = pSource.m_nMatchingAddresses;
+    }
+}
+
+void MemBlock::ExcludeMatchingAddress(ra::ByteAddress nAddress)
+{
+    const auto nAddressesSize = (m_nMaxAddresses + 7) / 8;
+    unsigned char* pAddresses = nullptr;
+    const auto nIndex = nAddress - m_nFirstAddress;
+    const auto nBit = 1 << (nIndex & 7);
+
+    if (AreAllAddressesMatching())
+    {
+        pAddresses = AllocateMatchingAddresses();
+        Expects(pAddresses != nullptr);
+        memset(pAddresses, 0xFF, nAddressesSize);
+    }
+    else
+    {
+        pAddresses = (nAddressesSize > sizeof(m_vAddresses)) ? m_pAddresses : &m_vAddresses[0];
+        Expects(pAddresses != nullptr);
+        if (!(pAddresses[nIndex >> 3] & nBit))
+            return;
+    }
+
+    pAddresses[nIndex >> 3] &= ~nBit;
+    --m_nMatchingAddresses;
+}
+
+bool MemBlock::ContainsAddress(ra::ByteAddress nAddress) const noexcept
+{
+    if (nAddress < m_nFirstAddress)
+        return false;
+
+    nAddress -= m_nFirstAddress;
+    return (nAddress < m_nMaxAddresses);
+}
+
+bool MemBlock::ContainsMatchingAddress(ra::ByteAddress nAddress) const
+{
+    if (nAddress < m_nFirstAddress)
+        return false;
+
+    const auto nIndex = nAddress - m_nFirstAddress;
+    if (nIndex >= m_nMaxAddresses)
+        return false;
+
+    if (AreAllAddressesMatching())
+        return true;
+
+    const uint8_t* pAddresses = GetMatchingAddressPointer();
+    Expects(pAddresses != nullptr);
+    const auto nBit = 1 << (nIndex & 7);
+    return (pAddresses[nIndex >> 3] & nBit);
+}
+
+ra::ByteAddress MemBlock::GetMatchingAddress(gsl::index nIndex) const noexcept
+{
+    if (AreAllAddressesMatching())
+        return m_nFirstAddress + gsl::narrow_cast<ra::ByteAddress>(nIndex);
+
+    const auto nAddressesSize = (m_nMaxAddresses + 7) / 8;
+    const uint8_t* pAddresses = (nAddressesSize > sizeof(m_vAddresses)) ? m_pAddresses : &m_vAddresses[0];
+    ra::ByteAddress nAddress = m_nFirstAddress;
+    const ra::ByteAddress nStop = m_nFirstAddress + m_nMaxAddresses;
+    uint8_t nMask = 0x01;
+
+    if (pAddresses != nullptr)
+    {
+        do
+        {
+            if (*pAddresses & nMask)
+            {
+                if (nIndex-- == 0)
+                    return nAddress;
+            }
+
+            if (nMask == 0x80)
+            {
+                nMask = 0x01;
+                pAddresses++;
+            }
+            else
+            {
+                nMask <<= 1;
+            }
+
+            nAddress++;
+        } while (nAddress < nStop);
+    }
+
+    return 0;
+}
+
+
 } // namespace impl
 
 _CONSTANT_VAR MAX_BLOCK_SIZE = 256U * 1024; // 256K
@@ -746,8 +1177,10 @@ void SearchResults::Initialize(ra::ByteAddress nAddress, size_t nBytes, SearchTy
     while (nBytes > 0)
     {
         const auto nBlockSize = gsl::narrow_cast<unsigned int>((nBytes > MAX_BLOCK_SIZE) ? MAX_BLOCK_SIZE : nBytes);
-        auto& block = m_vBlocks.emplace_back(nAddress, nBlockSize + nPadding);
-        pEmulatorContext.ReadMemory(block.GetAddress(), block.GetBytes(), gsl::narrow_cast<size_t>(block.GetSize()));
+        const auto nMaxAddresses = m_pImpl->GetAddressCountForBytes(nBlockSize + nPadding);
+        const auto nBlockAddress = m_pImpl->ConvertFromRealAddress(nAddress);
+        auto& block = m_vBlocks.emplace_back(nBlockAddress, nBlockSize + nPadding, nMaxAddresses);
+        pEmulatorContext.ReadMemory(nAddress, block.GetBytes(), gsl::narrow_cast<size_t>(block.GetBytesSize()));
 
         nAddress += nBlockSize;
         nBytes -= nBlockSize;
@@ -761,20 +1194,8 @@ bool SearchResults::Result::Compare(unsigned int nPreviousValue, ComparisonType
 
 bool SearchResults::ContainsAddress(ra::ByteAddress nAddress) const
 {
-    if (m_nFilterType != SearchFilterType::None)
-        return m_pImpl->ContainsAddress(m_vMatchingAddresses, nAddress);
-
-    if (!m_pImpl->IsAddressValid(nAddress))
-        return false;
-
-    const unsigned int nPadding = m_pImpl->GetPadding();
-    for (const auto& block : m_vBlocks)
-    {
-        if (block.GetAddress() > nAddress)
-            return false;
-        if (nAddress < block.GetAddress() + block.GetSize() - nPadding)
-            return true;
-    }
+    if (m_pImpl)
+        return m_pImpl->ContainsAddress(*this, nAddress);
 
     return false;
 }
@@ -813,7 +1234,6 @@ void SearchResults::Initialize(const SearchResults& srMemory, const SearchResult
 
 void SearchResults::MergeSearchResults(const SearchResults& srMemory, const SearchResults& srAddresses)
 {
-    m_vMatchingAddresses = srAddresses.m_vMatchingAddresses;
     m_vBlocks.reserve(srAddresses.m_vBlocks.size());
     m_nType = srAddresses.m_nType;
     m_pImpl = srAddresses.m_pImpl;
@@ -823,17 +1243,18 @@ void SearchResults::MergeSearchResults(const SearchResults& srMemory, const Sear
 
     for (const auto pSrcBlock : srAddresses.m_vBlocks)
     {
-        unsigned int nSize = pSrcBlock.GetSize();
-        ra::ByteAddress nAddress = pSrcBlock.GetAddress();
-        auto& pNewBlock = m_vBlocks.emplace_back(nAddress, nSize);
+        unsigned int nSize = pSrcBlock.GetBytesSize();
+        ra::ByteAddress nAddress = pSrcBlock.GetFirstAddress();
+        const auto nMaxAddresses = pSrcBlock.GetMaxAddresses();
+        auto& pNewBlock = m_vBlocks.emplace_back(nAddress, nSize, nMaxAddresses);
         unsigned char* pWrite = pNewBlock.GetBytes();
 
         for (const auto pMemBlock : srMemory.m_vBlocks)
         {
-            if (nAddress >= pMemBlock.GetAddress() && nAddress < pMemBlock.GetAddress() + pMemBlock.GetSize())
+            if (nAddress >= pMemBlock.GetFirstAddress() && nAddress < pMemBlock.GetFirstAddress() + pMemBlock.GetBytesSize())
             {
-                const auto nOffset = nAddress - pMemBlock.GetAddress();
-                const auto nAvailable = pMemBlock.GetSize() - nOffset;
+                const auto nOffset = nAddress - pMemBlock.GetFirstAddress();
+                const auto nAvailable = pMemBlock.GetBytesSize() - nOffset;
                 if (nAvailable >= nSize)
                 {
                     memcpy(pWrite, pMemBlock.GetBytes() + nOffset, nSize);
@@ -885,42 +1306,29 @@ void SearchResults::Initialize(const SearchResults& srMemory, const SearchResult
 
 size_t SearchResults::MatchingAddressCount() const noexcept
 {
-    if (m_nFilterType != SearchFilterType::None)
-        return m_vMatchingAddresses.size();
-
-    if (m_pImpl == nullptr)
-        return 0;
-
-    const unsigned int nPadding = m_pImpl->GetPadding();
     size_t nCount = 0;
-    for (auto& block : m_vBlocks)
-        nCount += gsl::narrow_cast<size_t>(block.GetSize()) - nPadding;
+    for (const auto& pBlock : m_vBlocks)
+        nCount += pBlock.GetMatchingAddressCount();
 
-    return m_pImpl->AdjustInitialAddressCount(nCount);
+    return nCount;
 }
 
-void SearchResults::ExcludeAddress(ra::ByteAddress nAddress) noexcept
+void SearchResults::ExcludeAddress(ra::ByteAddress nAddress)
 {
-    if (m_nFilterType != SearchFilterType::None)
+    if (m_nFilterType == SearchFilterType::None)
+        return;
+
+    for (auto& pBlock : m_vBlocks)
     {
-        for (auto iter = m_vMatchingAddresses.begin(); iter != m_vMatchingAddresses.end(); ++iter)
+        if (pBlock.ContainsAddress(nAddress))
         {
-            if (*iter == nAddress)
-            {
-                m_vMatchingAddresses.erase(iter);
-                break;
-            }
+            pBlock.ExcludeMatchingAddress(nAddress);
+            break;
         }
     }
 }
 
-void SearchResults::ExcludeMatchingAddress(gsl::index nIndex) noexcept
-{
-    if (m_nFilterType != SearchFilterType::None)
-        m_vMatchingAddresses.erase(m_vMatchingAddresses.begin() + nIndex);
-}
-
-bool SearchResults::GetMatchingAddress(gsl::index nIndex, _Out_ SearchResults::Result& result) const
+bool SearchResults::GetMatchingAddress(gsl::index nIndex, _Out_ SearchResults::Result& result) const noexcept
 {
     if (m_pImpl == nullptr)
     {
@@ -938,20 +1346,20 @@ bool SearchResults::GetBytes(ra::ByteAddress nAddress, unsigned char* pBuffer, s
         const unsigned int nPadding = m_pImpl->GetPadding();
         for (auto& block : m_vBlocks)
         {
-            if (block.GetAddress() > nAddress)
+            if (block.GetFirstAddress() > nAddress)
                 break;
 
-            const int nRemaining = ra::to_signed(block.GetAddress() + block.GetSize() - nAddress);
+            const int nRemaining = ra::to_signed(block.GetFirstAddress() + block.GetBytesSize() - nAddress);
             if (nRemaining < 0)
                 continue;
 
             if (ra::to_unsigned(nRemaining) >= nCount)
             {
-                memcpy(pBuffer, block.GetBytes() + (nAddress - block.GetAddress()), nCount);
+                memcpy(pBuffer, block.GetBytes() + (nAddress - block.GetFirstAddress()), nCount);
                 return true;
             }
 
-            memcpy(pBuffer, block.GetBytes() + (nAddress - block.GetAddress()), nRemaining);
+            memcpy(pBuffer, block.GetBytes() + (nAddress - block.GetFirstAddress()), nRemaining);
             nCount -= nRemaining;
             pBuffer += nRemaining;
             nAddress += nRemaining;
diff --git a/src/services/SearchResults.h b/src/services/SearchResults.h
index 5b2b0334..da71815f 100644
--- a/src/services/SearchResults.h
+++ b/src/services/SearchResults.h
@@ -37,68 +37,138 @@ namespace impl {
 class MemBlock
 {
 public:
-    explicit MemBlock(_In_ unsigned int nAddress, _In_ unsigned int nSize) noexcept :
-        m_nAddress(nAddress),
-        m_nSize(nSize)
+    explicit MemBlock(_In_ unsigned int nAddress, _In_ unsigned int nSize, _In_ unsigned int nMaxAddresses) noexcept :
+        m_nFirstAddress(nAddress),
+        m_nBytesSize(nSize),
+        m_nMatchingAddresses(nMaxAddresses),
+        m_nMaxAddresses(nMaxAddresses)
     {
         if (nSize > sizeof(m_vBytes))
-            m_pBytes = new (std::nothrow) unsigned char[nSize];
+            m_pBytes = new (std::nothrow) uint8_t[nSize];
     }
 
-    MemBlock(const MemBlock& other) noexcept : MemBlock(other.m_nAddress, other.m_nSize)
+    MemBlock(const MemBlock& other) noexcept : MemBlock(other.m_nFirstAddress, other.m_nBytesSize, other.m_nMaxAddresses)
     {
-        if (m_nSize > sizeof(m_vBytes))
-            std::memcpy(m_pBytes, other.m_pBytes, m_nSize);
+        if (m_nBytesSize > sizeof(m_vBytes))
+            std::memcpy(m_pBytes, other.m_pBytes, m_nBytesSize);
         else
             std::memcpy(m_vBytes, other.m_vBytes, sizeof(m_vBytes));
+
+        m_nMatchingAddresses = other.m_nMatchingAddresses;
+        if (!AreAllAddressesMatching())
+        {
+            if ((m_nMatchingAddresses + 7) / 8 > sizeof(m_vAddresses))
+                std::memcpy(m_pAddresses, other.m_pAddresses, (m_nMatchingAddresses + 7) / 8);
+            else
+                std::memcpy(m_vAddresses, other.m_vAddresses, sizeof(m_vAddresses));
+        }
     }
 
     MemBlock& operator=(const MemBlock&) noexcept = delete;
 
     MemBlock(MemBlock&& other) noexcept :
-        m_nAddress(other.m_nAddress),
-        m_nSize(other.m_nSize)
+        m_nFirstAddress(other.m_nFirstAddress),
+        m_nBytesSize(other.m_nBytesSize),
+        m_nMatchingAddresses(other.m_nMatchingAddresses),
+        m_nMaxAddresses(other.m_nMaxAddresses)
     {
-        if (other.m_nSize > sizeof(m_vBytes))
+        if (other.m_nBytesSize > sizeof(m_vBytes))
         {
             m_pBytes = other.m_pBytes;
             other.m_pBytes = nullptr;
-            other.m_nSize = 0;
+            other.m_nBytesSize = 0;
         }
         else
         {
             std::memcpy(m_vBytes, other.m_vBytes, sizeof(m_vBytes));
         }
+
+        if (!AreAllAddressesMatching())
+        {
+            if ((m_nMatchingAddresses + 7) / 8 > sizeof(m_vAddresses))
+            {
+                m_pAddresses = other.m_pAddresses;
+                other.m_pAddresses = nullptr;
+                other.m_nMatchingAddresses = 0;
+            }
+            else
+            {
+                std::memcpy(m_vAddresses, other.m_vAddresses, sizeof(m_vAddresses));
+            }
+        }
     }
 
     MemBlock& operator=(MemBlock&&) noexcept = delete;
 
     ~MemBlock() noexcept
     {
-        if (m_nSize > sizeof(m_vBytes))
+        if (m_nBytesSize > sizeof(m_vBytes))
             delete[] m_pBytes;
+        if (!AreAllAddressesMatching() && (m_nMatchingAddresses + 7) / 8 > sizeof(m_vAddresses))
+            delete[] m_pAddresses;
+    }
+
+    uint8_t* GetBytes() noexcept { return (m_nBytesSize > sizeof(m_vBytes)) ? m_pBytes : &m_vBytes[0]; }
+    const uint8_t* GetBytes() const noexcept { return (m_nBytesSize > sizeof(m_vBytes)) ? m_pBytes : &m_vBytes[0]; }
+
+    ra::ByteAddress GetFirstAddress() const noexcept { return m_nFirstAddress; }
+    unsigned int GetBytesSize() const noexcept { return m_nBytesSize; }
+    unsigned int GetMaxAddresses() const noexcept { return m_nMaxAddresses; }
+
+    bool ContainsAddress(ra::ByteAddress nAddress) const noexcept;
+
+    void SetMatchingAddresses(std::vector<ra::ByteAddress>& vAddresses, gsl::index nFirstIndex, gsl::index nLastIndex);
+    void CopyMatchingAddresses(const MemBlock& pSource);
+    void ExcludeMatchingAddress(ra::ByteAddress nAddress);
+    bool ContainsMatchingAddress(ra::ByteAddress nAddress) const;
+
+    unsigned int GetMatchingAddressCount() const noexcept { return m_nMatchingAddresses; }
+    ra::ByteAddress GetMatchingAddress(gsl::index nIndex) const noexcept;
+    bool AreAllAddressesMatching() const noexcept { return m_nMatchingAddresses == m_nMaxAddresses; }
+
+    const uint8_t* GetMatchingAddressPointer() const noexcept
+    {
+        if (AreAllAddressesMatching())
+            return nullptr;
+
+        const auto nAddressesSize = (m_nMaxAddresses + 7) / 8;
+        return (nAddressesSize > sizeof(m_vAddresses)) ? m_pAddresses : &m_vAddresses[0];
     }
 
-    unsigned char* GetBytes() noexcept { return (m_nSize > sizeof(m_vBytes)) ? m_pBytes : &m_vBytes[0]; }
-    const unsigned char* GetBytes() const noexcept { return (m_nSize > sizeof(m_vBytes)) ? m_pBytes : &m_vBytes[0]; }
+    bool HasMatchingAddress(const uint8_t* pMatchingAddresses, ra::ByteAddress nAddress) const noexcept
+    {
+        if (!pMatchingAddresses)
+            return true;
 
-    unsigned char GetByte(std::size_t nIndex) noexcept { return GetBytes()[nIndex]; }
-    unsigned char GetByte(std::size_t nIndex) const noexcept { return GetBytes()[nIndex]; }
+        const auto nIndex = nAddress - m_nFirstAddress;
+        if (nIndex >= m_nMaxAddresses)
+            return false;
 
-    ra::ByteAddress GetAddress() const noexcept { return m_nAddress; }
-    unsigned int GetSize() const noexcept { return m_nSize; }
+        const auto nBit = 1 << (nIndex & 7);
+        return (pMatchingAddresses[nIndex >> 3] & nBit);
+    }
 
 private:
+    uint8_t* AllocateMatchingAddresses() noexcept;
+
     union // 8 bytes
     {
-        unsigned char m_vBytes[8]{};
-        unsigned char* m_pBytes;
+        uint8_t m_vBytes[8]{};
+        uint8_t* m_pBytes;
     };
 
-    ra::ByteAddress m_nAddress; // 4 bytes
-    unsigned int m_nSize;       // 4 bytes
+    union // 8 bytes
+    {
+        uint8_t m_vAddresses[8]{};
+        uint8_t* m_pAddresses;
+    };
+
+    unsigned int m_nBytesSize;         // 4 bytes
+    ra::ByteAddress m_nFirstAddress;   // 4 bytes
+    unsigned int m_nMatchingAddresses; // 4 bytes
+    unsigned int m_nMaxAddresses;      // 4 bytes
 };
-static_assert(sizeof(MemBlock) == 16, "sizeof(MemBlock) is incorrect");
+static_assert(sizeof(MemBlock) <= 32, "sizeof(MemBlock) is incorrect");
 
 class SearchImpl;
 
@@ -177,7 +247,7 @@ public:
     /// </summary>
     /// <param name="result">The result.</param>
     /// <returns><c>true</c> if result was populated, <c>false</c> if the index was invalid.</returns>
-    bool GetMatchingAddress(gsl::index nIndex, _Out_ Result& result) const;
+    bool GetMatchingAddress(gsl::index nIndex, _Out_ Result& result) const noexcept;
 
     /// <summary>
     /// Gets the raw bytes at the specified address.
@@ -242,13 +312,7 @@ public:
     /// Removes an address from the matching address list.
     /// </summary>
     /// <param name="nAddress">The address to remove.</param>
-    void ExcludeAddress(ra::ByteAddress nAddress) noexcept;
-
-    /// <summary>
-    /// Removes an address from the matching address list.
-    /// </summary>
-    /// <param name="nAddress">The index of the address to remove.</param>
-    void ExcludeMatchingAddress(gsl::index nIndex) noexcept;
+    void ExcludeAddress(ra::ByteAddress nAddress);
 
 private:
     void MergeSearchResults(const SearchResults& srMemory, const SearchResults& srAddresses);
@@ -259,7 +323,6 @@ private:
     friend class impl::SearchImpl;
     impl::SearchImpl* m_pImpl = nullptr;
 
-    std::vector<ra::ByteAddress> m_vMatchingAddresses;
     ComparisonType m_nCompareType = ComparisonType::Equals;
     SearchFilterType m_nFilterType = SearchFilterType::None;
     unsigned int m_nFilterValue = 0U;


Test patch:
diff --git a/tests/services/SearchResults_Tests.cpp b/tests/services/SearchResults_Tests.cpp
index 2d64f4f3..797ca0aa 100644
--- a/tests/services/SearchResults_Tests.cpp
+++ b/tests/services/SearchResults_Tests.cpp
@@ -219,7 +219,7 @@ public:
         Assert::AreEqual(0x1234AB56U, result.nValue);
     }
 
-    TEST_METHOD(TestInitializeFromMemoryLowerNibble)
+    TEST_METHOD(TestInitializeFromMemoryNibble)
     {
         std::array<unsigned char, 5> memory{0x00, 0x12, 0x34, 0xAB, 0x56};
         ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
@@ -313,23 +313,67 @@ public:
         results1.Initialize(1U, 3U, ra::services::SearchType::EightBit);
         Assert::AreEqual({ 3U }, results1.MatchingAddressCount());
 
-        SearchResults results;
-        results.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::Constant, 0xABU);
+        SearchResults results2;
+        results2.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::Constant, 0xABU);
 
-        Assert::AreEqual({ 1U }, results.MatchingAddressCount());
-        Assert::IsFalse(results.ContainsAddress(0U));
-        Assert::IsFalse(results.ContainsAddress(1U));
-        Assert::IsFalse(results.ContainsAddress(2U));
-        Assert::IsTrue(results.ContainsAddress(3U));
-        Assert::IsFalse(results.ContainsAddress(4U));
+        Assert::AreEqual({ 1U }, results2.MatchingAddressCount());
+        Assert::IsFalse(results2.ContainsAddress(0U));
+        Assert::IsFalse(results2.ContainsAddress(1U));
+        Assert::IsFalse(results2.ContainsAddress(2U));
+        Assert::IsTrue(results2.ContainsAddress(3U));
+        Assert::IsFalse(results2.ContainsAddress(4U));
 
         SearchResults::Result result;
-        Assert::IsTrue(results.GetMatchingAddress(0U, result));
+        Assert::IsTrue(results2.GetMatchingAddress(0U, result));
         Assert::AreEqual(3U, result.nAddress);
         Assert::AreEqual(MemSize::EightBit, result.nSize);
         Assert::AreEqual(0xABU, result.nValue);
     }
 
+    TEST_METHOD(TestInitializeFromResultsEightBitEqualsConstantGap)
+    {
+        std::array<unsigned char, 6> memory{ 0x00, 0x12, 0x00, 0x00, 0x56, 0x00 };
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results1;
+        results1.Initialize(0U, 6U, ra::services::SearchType::EightBit);
+        Assert::AreEqual({ 6U }, results1.MatchingAddressCount());
+
+        SearchResults results2;
+        results2.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::Constant, 0x00U);
+        Assert::AreEqual({ 4U }, results2.MatchingAddressCount());
+
+        memory.at(1) = 0x00; // not previously matched, should be ignored
+        memory.at(3) = 0x01; // previously matched, should no longer match
+        SearchResults results3;
+        results3.Initialize(results2, ComparisonType::Equals, ra::services::SearchFilterType::Constant, 0x00U);
+
+        Assert::AreEqual({ 3U }, results3.MatchingAddressCount());
+        Assert::IsTrue(results3.ContainsAddress(0U));
+        Assert::IsFalse(results3.ContainsAddress(1U));
+        Assert::IsTrue(results3.ContainsAddress(2U));
+        Assert::IsFalse(results3.ContainsAddress(3U));
+        Assert::IsFalse(results3.ContainsAddress(4U));
+        Assert::IsTrue(results3.ContainsAddress(5U));
+
+        SearchResults::Result result;
+        Assert::IsTrue(results3.GetMatchingAddress(0U, result));
+        Assert::AreEqual(0U, result.nAddress);
+        Assert::AreEqual(MemSize::EightBit, result.nSize);
+        Assert::AreEqual(0x00U, result.nValue);
+
+        Assert::IsTrue(results3.GetMatchingAddress(1U, result));
+        Assert::AreEqual(2U, result.nAddress);
+        Assert::AreEqual(MemSize::EightBit, result.nSize);
+        Assert::AreEqual(0x00U, result.nValue);
+
+        Assert::IsTrue(results3.GetMatchingAddress(2U, result));
+        Assert::AreEqual(5U, result.nAddress);
+        Assert::AreEqual(MemSize::EightBit, result.nSize);
+        Assert::AreEqual(0x00U, result.nValue);
+    }
+
     TEST_METHOD(TestInitializeFromResultsEightBitNotEqualsConstant)
     {
         std::array<unsigned char, 5> memory{0x00, 0x12, 0x34, 0xAB, 0x56};
@@ -542,6 +586,63 @@ public:
         Assert::AreEqual(0xABU, result.nValue);
     }
 
+    TEST_METHOD(TestInitializeFromResultsEightBitEqualsPrevious)
+    {
+        std::array<unsigned char, 5> memory{ 0x00, 0x12, 0x34, 0xAB, 0x56 };
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results1;
+        results1.Initialize(0U, 5U, ra::services::SearchType::EightBit);
+        Assert::AreEqual({ 5U }, results1.MatchingAddressCount());
+
+        memory.at(1) = 0x14;
+        memory.at(2) = 0x55;
+        SearchResults results2;
+        results2.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::LastKnownValue, 0U);
+
+        Assert::AreEqual({ 3U }, results2.MatchingAddressCount());
+        Assert::IsTrue(results2.ContainsAddress(0U));
+        Assert::IsFalse(results2.ContainsAddress(1U));
+        Assert::IsFalse(results2.ContainsAddress(2U));
+        Assert::IsTrue(results2.ContainsAddress(3U));
+        Assert::IsTrue(results2.ContainsAddress(4U));
+
+        // no change - tests the "entire block matches" optimization
+        SearchResults results3;
+        results3.Initialize(results2, ComparisonType::Equals, ra::services::SearchFilterType::LastKnownValue, 0U);
+
+        Assert::AreEqual({ 3U }, results3.MatchingAddressCount());
+        Assert::IsTrue(results3.ContainsAddress(0U));
+        Assert::IsFalse(results3.ContainsAddress(1U));
+        Assert::IsFalse(results3.ContainsAddress(2U));
+        Assert::IsTrue(results3.ContainsAddress(3U));
+        Assert::IsTrue(results3.ContainsAddress(4U));
+
+        SearchResults::Result result;
+        Assert::IsTrue(results2.GetMatchingAddress(1U, result));
+        Assert::AreEqual(3U, result.nAddress);
+        Assert::AreEqual(MemSize::EightBit, result.nSize);
+        Assert::AreEqual(0xABU, result.nValue);
+
+        // no change - tests the "entire block matches" optimization
+        SearchResults results4;
+        memory.at(3) = 0x99;
+        results4.Initialize(results3, ComparisonType::Equals, ra::services::SearchFilterType::LastKnownValue, 0U);
+
+        Assert::AreEqual({ 2U }, results4.MatchingAddressCount());
+        Assert::IsTrue(results4.ContainsAddress(0U));
+        Assert::IsFalse(results4.ContainsAddress(1U));
+        Assert::IsFalse(results4.ContainsAddress(2U));
+        Assert::IsFalse(results4.ContainsAddress(3U));
+        Assert::IsTrue(results4.ContainsAddress(4U));
+
+        Assert::IsTrue(results4.GetMatchingAddress(1U, result));
+        Assert::AreEqual(4U, result.nAddress);
+        Assert::AreEqual(MemSize::EightBit, result.nSize);
+        Assert::AreEqual(0x56U, result.nValue);
+    }
+
     TEST_METHOD(TestInitializeFromResultsEightBitNotEqualsPrevious)
     {
         std::array<unsigned char, 5> memory{0x00, 0x12, 0x34, 0xAB, 0x56};
@@ -554,26 +655,86 @@ public:
 
         memory.at(1) = 0x14;
         memory.at(2) = 0x55;
-        SearchResults results;
-        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
+        SearchResults results2;
+        results2.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
 
-        Assert::AreEqual({ 2U }, results.MatchingAddressCount());
-        Assert::IsFalse(results.ContainsAddress(0U));
-        Assert::IsTrue(results.ContainsAddress(1U));
-        Assert::IsTrue(results.ContainsAddress(2U));
-        Assert::IsFalse(results.ContainsAddress(3U));
-        Assert::IsFalse(results.ContainsAddress(4U));
+        Assert::AreEqual({ 2U }, results2.MatchingAddressCount());
+        Assert::IsFalse(results2.ContainsAddress(0U));
+        Assert::IsTrue(results2.ContainsAddress(1U));
+        Assert::IsTrue(results2.ContainsAddress(2U));
+        Assert::IsFalse(results2.ContainsAddress(3U));
+        Assert::IsFalse(results2.ContainsAddress(4U));
 
         SearchResults::Result result;
-        Assert::IsTrue(results.GetMatchingAddress(0U, result));
+        Assert::IsTrue(results2.GetMatchingAddress(0U, result));
         Assert::AreEqual(1U, result.nAddress);
         Assert::AreEqual(MemSize::EightBit, result.nSize);
         Assert::AreEqual(0x14U, result.nValue);
 
-        Assert::IsTrue(results.GetMatchingAddress(1U, result));
+        Assert::IsTrue(results2.GetMatchingAddress(1U, result));
         Assert::AreEqual(2U, result.nAddress);
         Assert::AreEqual(MemSize::EightBit, result.nSize);
         Assert::AreEqual(0x55U, result.nValue);
+
+        // no change - tests the "entire block matches" optimization
+        SearchResults results3;
+        results3.Initialize(results2, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
+        Assert::AreEqual({ 0U }, results3.MatchingAddressCount());
+    }
+
+    TEST_METHOD(TestInitializeFromResultsEightBitNotEqualsPreviousLargeData)
+    {
+        std::array<unsigned char, 1024> memory{};
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        for (unsigned int i = 0; i < memory.size(); ++i)
+            GSL_SUPPRESS_BOUNDS4 memory[i] = (i % 256);
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results1;
+        results1.Initialize(0U, 1024U, ra::services::SearchType::EightBit);
+        Assert::AreEqual({ 1024U }, results1.MatchingAddressCount());
+
+        memory.at(1) = 0x14;
+        memory.at(4) = 0x22;
+        memory.at(12) = 0x55;
+        memory.at(99) = 0x99;
+        memory.at(700) = 0x00;
+        SearchResults results2;
+        results2.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
+
+        // expectation: results2 should allocate several smaller blocks instead of one large one
+        Assert::AreEqual({ 5U }, results2.MatchingAddressCount());
+        Assert::IsTrue(results2.ContainsAddress(1U));
+        Assert::IsTrue(results2.ContainsAddress(4U));
+        Assert::IsTrue(results2.ContainsAddress(12U));
+        Assert::IsTrue(results2.ContainsAddress(99U));
+        Assert::IsTrue(results2.ContainsAddress(700U));
+
+        SearchResults::Result result;
+        Assert::IsTrue(results2.GetMatchingAddress(0U, result));
+        Assert::AreEqual(1U, result.nAddress);
+        Assert::AreEqual(MemSize::EightBit, result.nSize);
+        Assert::AreEqual(0x14U, result.nValue);
+
+        Assert::IsTrue(results2.GetMatchingAddress(1U, result));
+        Assert::AreEqual(4U, result.nAddress);
+        Assert::AreEqual(MemSize::EightBit, result.nSize);
+        Assert::AreEqual(0x22U, result.nValue);
+
+        Assert::IsTrue(results2.GetMatchingAddress(2U, result));
+        Assert::AreEqual(12U, result.nAddress);
+        Assert::AreEqual(MemSize::EightBit, result.nSize);
+        Assert::AreEqual(0x55U, result.nValue);
+
+        Assert::IsTrue(results2.GetMatchingAddress(3U, result));
+        Assert::AreEqual(99U, result.nAddress);
+        Assert::AreEqual(MemSize::EightBit, result.nSize);
+        Assert::AreEqual(0x99U, result.nValue);
+
+        Assert::IsTrue(results2.GetMatchingAddress(4U, result));
+        Assert::AreEqual(700U, result.nAddress);
+        Assert::AreEqual(MemSize::EightBit, result.nSize);
+        Assert::AreEqual(0x00U, result.nValue);
     }
 
     TEST_METHOD(TestInitializeFromResultsEightBitEqualsPreviousPlusTwo)
@@ -1032,47 +1193,6 @@ public:
         Assert::AreEqual(0x55U, result.nValue);
     }
 
-    TEST_METHOD(TestExcludeMatchingAddressEightBit)
-    {
-        std::array<unsigned char, 5> memory{0x00, 0x12, 0x34, 0xAB, 0x56};
-        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
-        mockEmulatorContext.MockMemory(memory);
-
-        SearchResults results1;
-        results1.Initialize(1U, 3U, ra::services::SearchType::EightBit);
-        Assert::AreEqual({ 3U }, results1.MatchingAddressCount());
-
-        // exclude doesn't do anything to unfiltered results
-        results1.ExcludeMatchingAddress(0U);
-        Assert::AreEqual({ 3U }, results1.MatchingAddressCount());
-
-        memory.at(1) = 0x14;
-        memory.at(2) = 0x55;
-        SearchResults results;
-        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
-
-        Assert::AreEqual({ 2U }, results.MatchingAddressCount());
-        Assert::IsFalse(results.ContainsAddress(0U));
-        Assert::IsTrue(results.ContainsAddress(1U));
-        Assert::IsTrue(results.ContainsAddress(2U));
-        Assert::IsFalse(results.ContainsAddress(3U));
-        Assert::IsFalse(results.ContainsAddress(4U));
-
-        results.ExcludeMatchingAddress(0U);
-        Assert::AreEqual({ 1U }, results.MatchingAddressCount());
-        Assert::IsFalse(results.ContainsAddress(0U));
-        Assert::IsFalse(results.ContainsAddress(1U));
-        Assert::IsTrue(results.ContainsAddress(2U));
-        Assert::IsFalse(results.ContainsAddress(3U));
-        Assert::IsFalse(results.ContainsAddress(4U));
-
-        SearchResults::Result result;
-        Assert::IsTrue(results.GetMatchingAddress(0U, result));
-        Assert::AreEqual(2U, result.nAddress);
-        Assert::AreEqual(MemSize::EightBit, result.nSize);
-        Assert::AreEqual(0x55U, result.nValue);
-    }
-
     TEST_METHOD(TestInitializeFromMemoryEightBitLargeMemory)
     {
         auto memory = std::make_unique<unsigned char[]>(BIG_BLOCK_SIZE);
@@ -1350,34 +1470,89 @@ public:
         Assert::AreEqual({ 3U }, results1.MatchingAddressCount());
 
         // exclude doesn't do anything to unfiltered results
-        results1.ExcludeMatchingAddress(0U);
+        results1.ExcludeAddress(0U);
         Assert::AreEqual({ 3U }, results1.MatchingAddressCount());
 
         memory.at(1) = 0x14;
         memory.at(2) = 0x55;
-        SearchResults results;
-        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
+        SearchResults results2;
+        results2.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
+        Assert::AreEqual({ 2U }, results2.MatchingAddressCount());
 
-        SearchResults results2(results);
+        SearchResults results3(results2);
 
-        Assert::AreEqual({ 2U }, results2.MatchingAddressCount());
-        Assert::IsFalse(results2.ContainsAddress(0U));
-        Assert::IsTrue(results2.ContainsAddress(1U));
-        Assert::IsTrue(results2.ContainsAddress(2U));
-        Assert::IsFalse(results2.ContainsAddress(3U));
-        Assert::IsFalse(results2.ContainsAddress(4U));
+        Assert::AreEqual({ 2U }, results3.MatchingAddressCount());
+        Assert::IsFalse(results3.ContainsAddress(0U));
+        Assert::IsTrue(results3.ContainsAddress(1U));
+        Assert::IsTrue(results3.ContainsAddress(2U));
+        Assert::IsFalse(results3.ContainsAddress(3U));
+        Assert::IsFalse(results3.ContainsAddress(4U));
 
         SearchResults::Result result;
-        Assert::IsTrue(results.GetMatchingAddress(0U, result));
+        Assert::IsTrue(results3.GetMatchingAddress(0U, result));
         Assert::AreEqual(1U, result.nAddress);
         Assert::AreEqual(MemSize::EightBit, result.nSize);
         Assert::AreEqual(0x14U, result.nValue);
 
-        Assert::IsTrue(results.GetMatchingAddress(1U, result));
+        Assert::IsTrue(results3.GetMatchingAddress(1U, result));
         Assert::AreEqual(2U, result.nAddress);
         Assert::AreEqual(MemSize::EightBit, result.nSize);
         Assert::AreEqual(0x55U, result.nValue);
     }
+
+    TEST_METHOD(TestGetFormattedValueEightBit)
+    {
+        std::array<unsigned char, 5> memory{ 0x00, 0x02, 0x34, 0xAB, 0x56 };
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results1;
+        results1.Initialize(1U, 3U, ra::services::SearchType::EightBit);
+        Assert::AreEqual({ 3U }, results1.MatchingAddressCount());
+
+        Assert::AreEqual(std::wstring(L""), results1.GetFormattedValue(0U, MemSize::EightBit));
+        Assert::AreEqual(std::wstring(L"0x02"), results1.GetFormattedValue(1U, MemSize::EightBit));
+        Assert::AreEqual(std::wstring(L"0x34"), results1.GetFormattedValue(2U, MemSize::EightBit));
+        Assert::AreEqual(std::wstring(L"0xab"), results1.GetFormattedValue(3U, MemSize::EightBit));
+    }
+
+    TEST_METHOD(TestGetFormattedValueSixteenBit)
+    {
+        std::array<unsigned char, 5> memory{ 0x00, 0x02, 0x34, 0xAB, 0x56 };
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results1;
+        results1.Initialize(1U, 3U, ra::services::SearchType::SixteenBit);
+        Assert::AreEqual({ 2U }, results1.MatchingAddressCount());
+
+        Assert::AreEqual(std::wstring(L""), results1.GetFormattedValue(0U, MemSize::SixteenBit));
+        Assert::AreEqual(std::wstring(L"0x3402"), results1.GetFormattedValue(1U, MemSize::SixteenBit));
+        Assert::AreEqual(std::wstring(L"0xab34"), results1.GetFormattedValue(2U, MemSize::SixteenBit));
+        Assert::AreEqual(std::wstring(L""), results1.GetFormattedValue(3U, MemSize::SixteenBit));
+    }
+
+    TEST_METHOD(TestGetFormattedValueFourBit)
+    {
+        std::array<unsigned char, 5> memory{ 0x00, 0x02, 0x34, 0xAB, 0x56 };
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results1;
+        results1.Initialize(1U, 3U, ra::services::SearchType::FourBit);
+        Assert::AreEqual({ 6U }, results1.MatchingAddressCount());
+
+        Assert::AreEqual(std::wstring(L""), results1.GetFormattedValue(0U, MemSize::Nibble_Lower));
+        Assert::AreEqual(std::wstring(L""), results1.GetFormattedValue(0U, MemSize::Nibble_Upper));
+        Assert::AreEqual(std::wstring(L"0x2"), results1.GetFormattedValue(1U, MemSize::Nibble_Lower));
+        Assert::AreEqual(std::wstring(L"0x0"), results1.GetFormattedValue(1U, MemSize::Nibble_Upper));
+        Assert::AreEqual(std::wstring(L"0x4"), results1.GetFormattedValue(2U, MemSize::Nibble_Lower));
+        Assert::AreEqual(std::wstring(L"0x3"), results1.GetFormattedValue(2U, MemSize::Nibble_Upper));
+        Assert::AreEqual(std::wstring(L"0xb"), results1.GetFormattedValue(3U, MemSize::Nibble_Lower));
+        Assert::AreEqual(std::wstring(L"0xa"), results1.GetFormattedValue(3U, MemSize::Nibble_Upper));
+        Assert::AreEqual(std::wstring(L""), results1.GetFormattedValue(4U, MemSize::Nibble_Lower));
+        Assert::AreEqual(std::wstring(L""), results1.GetFormattedValue(4U, MemSize::Nibble_Upper));
+    }
 };
 
 } // namespace tests