Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[scudo] Reduce unsuccessful attempts of page releasing #110583

Merged
merged 3 commits into from
Oct 2, 2024

Conversation

ChiaHungDuan
Copy link
Contributor

We introduce a new strategy to track how many bytes are not released because of the contraint of release interval. This will change the TryReleaseThreshold adaptively so that we avoid releasing the same pages multiple times (and wasting time on the case of no pages to release).

On Android, the number of release attempts decreases 33% (572 to 382) and the worst case drops from 251 to 33. At the same time, it maintains almost the same RSS usage (with some improvements as well).

Note that in this CL, this is only applied to non small blocks. We will bring the strategy to all the size classes later.

We introduce a new strategy to track how many bytes are not released
because of the contraint of release interval. This will change the
`TryReleaseThreshold` adaptively so that we avoid releasing the same
pages multiple times (and wasting time on the case of no pages to
release).

On Android, the number of release attempts decreases 33% (572 to 382)
and the worst case drops from 251 to 33. At the same time, it maintains
almost the same RSS usage (with some improvements as well).

Note that in this CL, this is only applied to non small blocks. We will
bring the strategy to all the size classes later.
@llvmbot
Copy link
Collaborator

llvmbot commented Sep 30, 2024

@llvm/pr-subscribers-compiler-rt-sanitizer

Author: None (ChiaHungDuan)

Changes

We introduce a new strategy to track how many bytes are not released because of the contraint of release interval. This will change the TryReleaseThreshold adaptively so that we avoid releasing the same pages multiple times (and wasting time on the case of no pages to release).

On Android, the number of release attempts decreases 33% (572 to 382) and the worst case drops from 251 to 33. At the same time, it maintains almost the same RSS usage (with some improvements as well).

Note that in this CL, this is only applied to non small blocks. We will bring the strategy to all the size classes later.


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

1 Files Affected:

  • (modified) compiler-rt/lib/scudo/standalone/primary64.h (+80-39)
diff --git a/compiler-rt/lib/scudo/standalone/primary64.h b/compiler-rt/lib/scudo/standalone/primary64.h
index a3876479ec8158..2e50ba8142126b 100644
--- a/compiler-rt/lib/scudo/standalone/primary64.h
+++ b/compiler-rt/lib/scudo/standalone/primary64.h
@@ -532,6 +532,11 @@ template <typename Config> class SizeClassAllocator64 {
     uptr BytesInFreeListAtLastCheckpoint;
     uptr RangesReleased;
     uptr LastReleasedBytes;
+    // The minimum size of pushed blocks to trigger page release.
+    uptr TryReleaseThreshold;
+    // The number of bytes not triggering `releaseToOSMaybe()` because of
+    // the length of release interval.
+    uptr PendingPushedBytesDelta;
     u64 LastReleaseAtNs;
   };
 
@@ -560,8 +565,6 @@ template <typename Config> class SizeClassAllocator64 {
     u32 RandState GUARDED_BY(MMLock) = 0;
     BlocksInfo FreeListInfo GUARDED_BY(FLLock);
     PagesInfo MemMapInfo GUARDED_BY(MMLock);
-    // The minimum size of pushed blocks to trigger page release.
-    uptr TryReleaseThreshold GUARDED_BY(MMLock) = 0;
     ReleaseToOsInfo ReleaseInfo GUARDED_BY(MMLock) = {};
     bool Exhausted GUARDED_BY(MMLock) = false;
     bool isPopulatingFreeList GUARDED_BY(FLLock) = false;
@@ -610,9 +613,8 @@ template <typename Config> class SizeClassAllocator64 {
     return BlockSize < PageSize / 16U;
   }
 
-  ALWAYS_INLINE static bool isLargeBlock(uptr BlockSize) {
-    const uptr PageSize = getPageSizeCached();
-    return BlockSize > PageSize;
+  ALWAYS_INLINE uptr getMinReleaseAttemptSize(uptr BlockSize) {
+    return roundUp(BlockSize, getPageSizeCached());
   }
 
   ALWAYS_INLINE void initRegion(RegionInfo *Region, uptr ClassId,
@@ -631,12 +633,16 @@ template <typename Config> class SizeClassAllocator64 {
           (getRandomModN(&Region->RandState, 16) + 1) * PageSize;
     }
 
+    const uptr BlockSize = getSizeByClassId(ClassId);
     // Releasing small blocks is expensive, set a higher threshold to avoid
     // frequent page releases.
-    if (isSmallBlock(getSizeByClassId(ClassId)))
-      Region->TryReleaseThreshold = PageSize * SmallerBlockReleasePageDelta;
-    else
-      Region->TryReleaseThreshold = PageSize;
+    if (isSmallBlock(BlockSize)) {
+      Region->ReleaseInfo.TryReleaseThreshold =
+          PageSize * SmallerBlockReleasePageDelta;
+    } else {
+      Region->ReleaseInfo.TryReleaseThreshold =
+          getMinReleaseAttemptSize(BlockSize);
+    }
   }
 
   void pushBatchClassBlocks(RegionInfo *Region, CompactPtrT *Array, u32 Size)
@@ -1245,6 +1251,7 @@ template <typename Config> class SizeClassAllocator64 {
     uptr BytesInFreeList;
     const uptr AllocatedUserEnd =
         Region->MemMapInfo.AllocatedUser + Region->RegionBeg;
+    uptr RegionPushedBytesDelta = 0;
     SinglyLinkedList<BatchGroupT> GroupsToRelease;
 
     {
@@ -1267,6 +1274,12 @@ template <typename Config> class SizeClassAllocator64 {
         return 0;
       }
 
+      // Given that we will unlock the freelist for block operations, cache the
+      // value here so that when we are adapting the `TryReleaseThreshold`
+      // later, we are using the right metric.
+      RegionPushedBytesDelta =
+          BytesInFreeList - Region->ReleaseInfo.BytesInFreeListAtLastCheckpoint;
+
       // ==================================================================== //
       // 2. Determine which groups can release the pages. Use a heuristic to
       //    gather groups that are candidates for doing a release.
@@ -1310,12 +1323,36 @@ template <typename Config> class SizeClassAllocator64 {
     auto SkipRegion = [](UNUSED uptr RegionIndex) { return false; };
     releaseFreeMemoryToOS(Context, Recorder, SkipRegion);
     if (Recorder.getReleasedRangesCount() > 0) {
+      // This is the case that we didn't hit the release threshold but it has
+      // been past a certain period of time. Thus we try to release some pages
+      // and if it does release some additional pages, it's hint that we are
+      // able to lower the threshold.
+      // TODO(chiahungduan): Apply the same adjustment strategy to small blocks.
+      if (!isSmallBlock(BlockSize)) {
+        if (RegionPushedBytesDelta < Region->ReleaseInfo.TryReleaseThreshold &&
+            Recorder.getReleasedBytes() >
+                Region->ReleaseInfo.LastReleasedBytes +
+                    getMinReleaseAttemptSize(BlockSize)) {
+          Region->ReleaseInfo.TryReleaseThreshold =
+              Max(Region->ReleaseInfo.TryReleaseThreshold / 2,
+                  getMinReleaseAttemptSize(BlockSize));
+        }
+      }
+
       Region->ReleaseInfo.BytesInFreeListAtLastCheckpoint = BytesInFreeList;
       Region->ReleaseInfo.RangesReleased += Recorder.getReleasedRangesCount();
       Region->ReleaseInfo.LastReleasedBytes = Recorder.getReleasedBytes();
     }
     Region->ReleaseInfo.LastReleaseAtNs = getMonotonicTimeFast();
 
+    if (Region->ReleaseInfo.PendingPushedBytesDelta > 0) {
+      Region->ReleaseInfo.TryReleaseThreshold +=
+          Region->ReleaseInfo.PendingPushedBytesDelta / 2;
+      Region->ReleaseInfo.TryReleaseThreshold = Min<uptr>(
+          Region->ReleaseInfo.TryReleaseThreshold, (1UL << GroupSizeLog) / 2);
+      Region->ReleaseInfo.PendingPushedBytesDelta = 0;
+    }
+
     // ====================================================================== //
     // 5. Merge the `GroupsToRelease` back to the freelist.
     // ====================================================================== //
@@ -1329,8 +1366,6 @@ template <typename Config> class SizeClassAllocator64 {
       REQUIRES(Region->MMLock, Region->FLLock) {
     DCHECK_GE(Region->FreeListInfo.PoppedBlocks,
               Region->FreeListInfo.PushedBlocks);
-    const uptr PageSize = getPageSizeCached();
-
     // Always update `BytesInFreeListAtLastCheckpoint` with the smallest value
     // so that we won't underestimate the releasable pages. For example, the
     // following is the region usage,
@@ -1354,34 +1389,40 @@ template <typename Config> class SizeClassAllocator64 {
 
     const uptr RegionPushedBytesDelta =
         BytesInFreeList - Region->ReleaseInfo.BytesInFreeListAtLastCheckpoint;
-    if (RegionPushedBytesDelta < PageSize)
-      return false;
-
-    // Releasing smaller blocks is expensive, so we want to make sure that a
-    // significant amount of bytes are free, and that there has been a good
-    // amount of batches pushed to the freelist before attempting to release.
-    if (isSmallBlock(BlockSize) && ReleaseType == ReleaseToOS::Normal)
-      if (RegionPushedBytesDelta < Region->TryReleaseThreshold)
-        return false;
 
     if (ReleaseType == ReleaseToOS::Normal) {
-      const s32 IntervalMs = atomic_load_relaxed(&ReleaseToOsIntervalMs);
-      if (IntervalMs < 0)
+      if (RegionPushedBytesDelta < Region->ReleaseInfo.TryReleaseThreshold / 2)
+        return false;
+
+      const u64 IntervalNs =
+          static_cast<u64>(atomic_load_relaxed(&ReleaseToOsIntervalMs)) *
+          1000000;
+      if (IntervalNs < 0)
         return false;
 
-      // The constant 8 here is selected from profiling some apps and the number
-      // of unreleased pages in the large size classes is around 16 pages or
-      // more. Choose half of it as a heuristic and which also avoids page
-      // release every time for every pushBlocks() attempt by large blocks.
-      const bool ByPassReleaseInterval =
-          isLargeBlock(BlockSize) && RegionPushedBytesDelta > 8 * PageSize;
-      if (!ByPassReleaseInterval) {
-        if (Region->ReleaseInfo.LastReleaseAtNs +
-                static_cast<u64>(IntervalMs) * 1000000 >
-            getMonotonicTimeFast()) {
-          // Memory was returned recently.
+      const u64 CurTimeNs = getMonotonicTimeFast();
+      const u64 DiffSinceLastReleaseNs =
+          CurTimeNs - Region->ReleaseInfo.LastReleaseAtNs;
+
+      // When `RegionPushedBytesDelta` is more than half of
+      // `TryReleaseThreshold` and the last release was happened 2 release
+      // interval before, we will try to see if there's any chance to release
+      // some memory. If we do release additional memory, we decrease the
+      // threshold accodingly.
+      if (RegionPushedBytesDelta < Region->ReleaseInfo.TryReleaseThreshold) {
+        if (DiffSinceLastReleaseNs < 2 * IntervalNs)
           return false;
-        }
+      } else if (DiffSinceLastReleaseNs < IntervalNs) {
+        // `TryReleaseThreshold` will be adjusted according to how many bytes
+        // are not released due to the interval constraint and it will be
+        // increased so that we are likely to release them all in the next time.
+        // TODO(chiahungduan): Apply the same adjustment strategy to small
+        // blocks.
+        if (!isSmallBlock(BlockSize))
+          Region->ReleaseInfo.PendingPushedBytesDelta = RegionPushedBytesDelta;
+
+        // Memory was returned recently.
+        return false;
       }
     } // if (ReleaseType == ReleaseToOS::Normal)
 
@@ -1397,10 +1438,10 @@ template <typename Config> class SizeClassAllocator64 {
     SinglyLinkedList<BatchGroupT> GroupsToRelease;
 
     // We are examining each group and will take the minimum distance to the
-    // release threshold as the next Region::TryReleaseThreshold(). Note that if
-    // the size of free blocks has reached the release threshold, the distance
-    // to the next release will be PageSize * SmallerBlockReleasePageDelta. See
-    // the comment on `SmallerBlockReleasePageDelta` for more details.
+    // release threshold as the next `TryReleaseThreshold`. Note that if the
+    // size of free blocks has reached the release threshold, the distance to
+    // the next release will be PageSize * SmallerBlockReleasePageDelta. See the
+    // comment on `SmallerBlockReleasePageDelta` for more details.
     uptr MinDistToThreshold = GroupSize;
 
     for (BatchGroupT *BG = Region->FreeListInfo.BlockList.front(),
@@ -1548,7 +1589,7 @@ template <typename Config> class SizeClassAllocator64 {
       // back to normal.
       if (MinDistToThreshold == GroupSize)
         MinDistToThreshold = PageSize * SmallerBlockReleasePageDelta;
-      Region->TryReleaseThreshold = MinDistToThreshold;
+      Region->ReleaseInfo.TryReleaseThreshold = MinDistToThreshold;
     }
 
     return GroupsToRelease;

Copy link
Contributor

@cferris1000 cferris1000 left a comment

Choose a reason for hiding this comment

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

Is there any way to add some tests for this behavior?

Region->ReleaseInfo.LastReleasedBytes +
getMinReleaseAttemptSize(BlockSize)) {
Region->ReleaseInfo.TryReleaseThreshold =
Max(Region->ReleaseInfo.TryReleaseThreshold / 2,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a comment about why the divide by 2 is used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Region->ReleaseInfo.BytesInFreeListAtLastCheckpoint = BytesInFreeList;
Region->ReleaseInfo.RangesReleased += Recorder.getReleasedRangesCount();
Region->ReleaseInfo.LastReleasedBytes = Recorder.getReleasedBytes();
}
Region->ReleaseInfo.LastReleaseAtNs = getMonotonicTimeFast();

if (Region->ReleaseInfo.PendingPushedBytesDelta > 0) {
Region->ReleaseInfo.TryReleaseThreshold +=
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a comment here too about why these numbers were chosen?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

CurTimeNs - Region->ReleaseInfo.LastReleaseAtNs;

// When `RegionPushedBytesDelta` is more than half of
// `TryReleaseThreshold` and the last release was happened 2 release
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a bit confusing and doesn't completely match the code below. I'm not sure you need this comment since it's mostly what's happening and the code seems relatively self-explanatory. However, it would be nice to know why you chose the 2 * IntervalNs value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess the code structure doesn't give the idea clear enough. I added more comments. Let me know if it looks better

@ChiaHungDuan
Copy link
Contributor Author

Is there any way to add some tests for this behavior?

Right, I was thinking the same thing. I'm thinking of some refactoring the use of ReleaseRecorder so that we can count the number of page release attempts. Given that I still have few pending refactors for the page release, I may bring the tests later (I will evaluate how possible and reliable the test can be). Let me know if you think it's better to have it earlier, I can reorder the CLs.

Copy link
Contributor

@cferris1000 cferris1000 left a comment

Choose a reason for hiding this comment

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

Mostly nits left.

I think it's fine to add tests later since this is all so tricky.

CurTimeNs - Region->ReleaseInfo.LastReleaseAtNs;

// At here, `RegionPushedBytesDelta` is more than half of
// `TryReleaseThreshold`. If the last release was happened 2 release
Copy link
Contributor

Choose a reason for hiding this comment

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

last release was happened -> last release happened

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

// release some memory even it doesn't exceed the threshold.
if (RegionPushedBytesDelta < Region->ReleaseInfo.TryReleaseThreshold) {
// We want the threshold to have a shorter response time to the variant
// memory usage patterns. By having some experiments (which was done
Copy link
Contributor

Choose a reason for hiding this comment

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

According to data collected during experiments (which were done with ....)

Or something similar.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

return false;
}
} else if (DiffSinceLastReleaseNs < IntervalNs) {
// In this case, we are over the threshold but we just did some page
Copy link
Contributor

Choose a reason for hiding this comment

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

we just did some page -> we just released some pages in the same release interval.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link
Contributor

@cferris1000 cferris1000 left a comment

Choose a reason for hiding this comment

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

lgtm

@ChiaHungDuan ChiaHungDuan merged commit 36dff0d into llvm:main Oct 2, 2024
7 checks passed
Sterling-Augustine pushed a commit to Sterling-Augustine/llvm-project that referenced this pull request Oct 3, 2024
We introduce a new strategy to track how many bytes are not released
because of the contraint of release interval. This will change the
`TryReleaseThreshold` adaptively so that we avoid releasing the same
pages multiple times (and wasting time on the case of no pages to
release).

On Android, the number of release attempts decreases 33% (572 to 382)
and the worst case drops from 251 to 33. At the same time, it maintains
almost the same RSS usage (with some improvements as well).

Note that in this CL, this is only applied to non small blocks. We will
bring the strategy to all the size classes later.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants