-
Notifications
You must be signed in to change notification settings - Fork 68
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
Collect live bytes during GC #768
Changes from 2 commits
622ac80
18f677f
845a8e7
9218e23
fd80eb8
21284ab
76dc0fc
b772d27
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -124,6 +124,18 @@ impl<C: GCWorkContext + 'static> GCWork<C::VM> for Release<C> { | |||
let result = w.designated_work.push(Box::new(ReleaseCollector)); | ||||
debug_assert!(result.is_ok()); | ||||
} | ||||
|
||||
#[cfg(feature = "count_live_bytes_in_gc")] | ||||
{ | ||||
let live_bytes = mmtk | ||||
.scheduler | ||||
.worker_group | ||||
.get_and_clear_worker_live_bytes(); | ||||
self.plan | ||||
.base() | ||||
.live_bytes_in_last_gc | ||||
.store(live_bytes, std::sync::atomic::Ordering::SeqCst); | ||||
} | ||||
} | ||||
} | ||||
|
||||
|
@@ -232,6 +244,28 @@ impl<VM: VMBinding> GCWork<VM> for EndOfGC { | |||
self.elapsed.as_millis() | ||||
); | ||||
|
||||
#[cfg(feature = "count_live_bytes_in_gc")] | ||||
{ | ||||
let live_bytes = mmtk | ||||
.plan | ||||
.base() | ||||
.live_bytes_in_last_gc | ||||
.load(std::sync::atomic::Ordering::SeqCst); | ||||
let used_bytes = | ||||
mmtk.plan.get_used_pages() << crate::util::constants::LOG_BYTES_IN_PAGE; | ||||
debug_assert!( | ||||
live_bytes <= used_bytes, | ||||
"Live bytes of all live objects ({} bytes) is larger than used pages ({} bytes), something is wrong.", | ||||
live_bytes, used_bytes | ||||
); | ||||
info!( | ||||
"Live objects = {} bytes ({:04.1}% of {} used pages)", | ||||
live_bytes, | ||||
live_bytes as f64 * 100.0 / used_bytes as f64, | ||||
mmtk.plan.get_used_pages() | ||||
); | ||||
} | ||||
|
||||
// We assume this is the only running work packet that accesses plan at the point of execution | ||||
#[allow(clippy::cast_ref_to_mut)] | ||||
let plan_mut: &mut dyn Plan<VM = VM> = unsafe { &mut *(&*mmtk.plan as *const _ as *mut _) }; | ||||
|
@@ -323,6 +357,16 @@ impl<E: ProcessEdgesWork> ObjectTracerContext<E::VM> for ProcessEdgesWorkTracerC | |||
fn with_tracer<R, F>(&self, worker: &mut GCWorker<E::VM>, func: F) -> R | ||||
where | ||||
F: FnOnce(&mut Self::TracerType) -> R, | ||||
{ | ||||
self.with_tracer_and_worker(worker, |tracer, _| func(tracer)) | ||||
} | ||||
} | ||||
|
||||
impl<E: ProcessEdgesWork> ProcessEdgesWorkTracerContext<E> { | ||||
// This Also exposes worker to the callback function. This is not a public method. | ||||
fn with_tracer_and_worker<R, F>(&self, worker: &mut GCWorker<E::VM>, func: F) -> R | ||||
where | ||||
F: FnOnce(&mut ProcessEdgesWorkTracer<E>, &mut GCWorker<E::VM>) -> R, | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Strictly speaking, it is unsafe to give the user access to both the tracer and the worker. The tracer is a closure that calls See: Line 53 in 813fa99
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another problem with this is that VM bindings can use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The remaining question is how to count live bytes for weak references. As the weak reference processing is totally at the binding side, we may have to expose a method and let the binding to call it when they scan weak refs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If we count the object size at |
||||
{ | ||||
let mmtk = worker.mmtk; | ||||
|
||||
|
@@ -339,7 +383,7 @@ impl<E: ProcessEdgesWork> ObjectTracerContext<E::VM> for ProcessEdgesWorkTracerC | |||
}; | ||||
|
||||
// The caller can use the tracer here. | ||||
let result = func(&mut tracer); | ||||
let result = func(&mut tracer, worker); | ||||
|
||||
// Flush the queued nodes. | ||||
tracer.flush_if_not_empty(); | ||||
|
@@ -826,6 +870,12 @@ pub trait ScanObjectsWork<VM: VMBinding>: GCWork<VM> + Sized { | |||
// If an object supports edge-enqueuing, we enqueue its edges. | ||||
<VM as VMBinding>::VMScanning::scan_object(tls, object, &mut closure); | ||||
self.post_scan_object(object); | ||||
|
||||
#[cfg(feature = "count_live_bytes_in_gc")] | ||||
closure | ||||
.worker | ||||
.shared | ||||
.increase_live_bytes(VM::VMObjectModel::get_current_size(object)); | ||||
qinsoon marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
} else { | ||||
// If an object does not support edge-enqueuing, we have to use | ||||
// `Scanning::scan_object_and_trace_edges` and offload the job of updating the | ||||
|
@@ -845,7 +895,7 @@ pub trait ScanObjectsWork<VM: VMBinding>: GCWork<VM> + Sized { | |||
phantom_data: PhantomData, | ||||
}; | ||||
|
||||
object_tracer_context.with_tracer(worker, |object_tracer| { | ||||
object_tracer_context.with_tracer_and_worker(worker, |object_tracer, _worker| { | ||||
// Scan objects and trace their edges at the same time. | ||||
for object in scan_later.iter().copied() { | ||||
trace!("Scan object (node) {}", object); | ||||
|
@@ -855,6 +905,11 @@ pub trait ScanObjectsWork<VM: VMBinding>: GCWork<VM> + Sized { | |||
object_tracer, | ||||
); | ||||
self.post_scan_object(object); | ||||
|
||||
#[cfg(feature = "count_live_bytes_in_gc")] | ||||
_worker | ||||
.shared | ||||
.increase_live_bytes(VM::VMObjectModel::get_current_size(object)); | ||||
qinsoon marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
} | ||||
}); | ||||
} | ||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,6 +31,11 @@ pub fn current_worker_ordinal() -> Option<ThreadId> { | |
pub struct GCWorkerShared<VM: VMBinding> { | ||
/// Worker-local statistics data. | ||
stat: AtomicRefCell<WorkerLocalStat<VM>>, | ||
/// Accumulated bytes for live objects in this GC. When each worker scans | ||
/// objects, we increase the live bytes. We get this value from each worker | ||
/// at the end of a GC, and reset this counter. | ||
#[cfg(feature = "count_live_bytes_in_gc")] | ||
live_bytes: AtomicUsize, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need atomic for this? It's worker-local right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It is in the shared part of a worker. We cannot get a mutable reference to it. It needs to be in the shared part, as we need to iterate through all workers, and sum it up. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah OK. I was just thinking if we can make it cheap enough by not using atomic for worker local stats, we could enable this feature by default. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not make it worker-local and each worker reports the live bytes after their transitive closure? Then we don't have to explicitly iterate through all the workers, we just collate the numbers we get from the transitive closure. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It could be any stage after the transitive closure stages (including weak reference processing stages). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe it's easier to defer the "live byte count" to each policy as opposed to each worker? But then it stops being worker-local so it would need synchronization. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah. It need at least the same level synchronization as the current code. And the counting code is scattered to each policy. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I personally don't have a strong opinion. Keeping track of fragmentation is definitely useful though. If it's possible to do it cheaply for every GC, may as well do it. If it's not possible then I don't think should spend more time on it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @qinsoon Sorry about the typo. I meant |
||
/// A queue of GCWork that can only be processed by the owned thread. | ||
/// | ||
/// Note: Currently, designated work cannot be added from the GC controller thread, or | ||
|
@@ -45,10 +50,22 @@ impl<VM: VMBinding> GCWorkerShared<VM> { | |
pub fn new(stealer: Option<Stealer<Box<dyn GCWork<VM>>>>) -> Self { | ||
Self { | ||
stat: Default::default(), | ||
#[cfg(feature = "count_live_bytes_in_gc")] | ||
live_bytes: AtomicUsize::new(0), | ||
designated_work: ArrayQueue::new(16), | ||
stealer, | ||
} | ||
} | ||
|
||
#[cfg(feature = "count_live_bytes_in_gc")] | ||
pub(crate) fn increase_live_bytes(&self, bytes: usize) { | ||
self.live_bytes.fetch_add(bytes, Ordering::Relaxed); | ||
} | ||
|
||
#[cfg(feature = "count_live_bytes_in_gc")] | ||
pub(crate) fn get_and_clear_live_bytes(&self) -> usize { | ||
self.live_bytes.swap(0, Ordering::SeqCst) | ||
} | ||
} | ||
|
||
/// A GC worker. This part is privately owned by a worker thread. | ||
|
@@ -285,6 +302,14 @@ impl<VM: VMBinding> WorkerGroup<VM> { | |
.iter() | ||
.any(|w| !w.designated_work.is_empty()) | ||
} | ||
|
||
#[cfg(feature = "count_live_bytes_in_gc")] | ||
pub fn get_and_clear_worker_live_bytes(&self) -> usize { | ||
self.workers_shared | ||
.iter() | ||
.map(|w| w.get_and_clear_live_bytes()) | ||
.sum() | ||
} | ||
} | ||
|
||
/// This ensures the worker always decrements the parked worker count on all control flow paths. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This no longer needs to be
pub(crate)
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we still need this. When we do
worker.shared.increase_live_bytes()
inScanObjectsWork::do_work_common()
, we already createdObjectsClosure
which takes&mut GCWorker
. We can't useworker
directly, and we have to use it throughObjectsClosure
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, sorry. I didn't notice that.