Skip to content

Commit

Permalink
reduce memory overhead for step value types (#1158)
Browse files Browse the repository at this point in the history
Use AtomicLongFieldUpdater to shrink the footprint of StepLong, StepDouble,
and AtomicDouble. This avoids the object overhead and pointer traversal
needed when using AtomicLong internally. 

Note, it does change some of the public methods on StepLong and StepDouble.
However, those are internal implementation classes and should not be used
outside of the spectator libraries.

---------

Co-authored-by: Brian Harrington <brharrington@gmail.com>
  • Loading branch information
kilink and brharrington authored Oct 11, 2024
1 parent 04a6b4d commit 17e6d74
Show file tree
Hide file tree
Showing 11 changed files with 635 additions and 119 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 Netflix, Inc.
* Copyright 2014-2024 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -15,7 +15,7 @@
*/
package com.netflix.spectator.impl;

import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;

/**
* Wrapper around AtomicLong to make working with double values easier.
Expand All @@ -26,22 +26,25 @@
@SuppressWarnings("PMD.MissingSerialVersionUID")
public class AtomicDouble extends Number {

private final AtomicLong value;
private volatile long value;

private static final AtomicLongFieldUpdater<AtomicDouble> VALUE_UPDATER =
AtomicLongFieldUpdater.newUpdater(AtomicDouble.class, "value");

/** Create an instance with an initial value of 0. */
public AtomicDouble() {
this(0.0);
super();
}

/** Create an instance with an initial value of {@code init}. */
public AtomicDouble(double init) {
super();
value = new AtomicLong(Double.doubleToLongBits(init));
this.value = Double.doubleToLongBits(init);
}

/** Return the current value. */
public double get() {
return Double.longBitsToDouble(value.get());
return Double.longBitsToDouble(value);
}

/** Add {@code amount} to the value and return the new value. */
Expand All @@ -51,11 +54,11 @@ public double addAndGet(double amount) {
double n;
long next;
do {
v = value.get();
v = value;
d = Double.longBitsToDouble(v);
n = d + amount;
next = Double.doubleToLongBits(n);
} while (!value.compareAndSet(v, next));
} while (!VALUE_UPDATER.compareAndSet(this, v, next));
return n;
}

Expand All @@ -66,17 +69,17 @@ public double getAndAdd(double amount) {
double n;
long next;
do {
v = value.get();
v = value;
d = Double.longBitsToDouble(v);
n = d + amount;
next = Double.doubleToLongBits(n);
} while (!value.compareAndSet(v, next));
} while (!VALUE_UPDATER.compareAndSet(this, v, next));
return d;
}

/** Set the value to {@code amount} and return the previous value. */
public double getAndSet(double amount) {
long v = value.getAndSet(Double.doubleToLongBits(amount));
long v = VALUE_UPDATER.getAndSet(this, Double.doubleToLongBits(amount));
return Double.longBitsToDouble(v);
}

Expand All @@ -87,15 +90,29 @@ public double getAndSet(double amount) {
public boolean compareAndSet(double expect, double update) {
long e = Double.doubleToLongBits(expect);
long u = Double.doubleToLongBits(update);
return value.compareAndSet(e, u);
return VALUE_UPDATER.compareAndSet(this, e, u);
}

/** Set the current value to {@code amount}. */
public void set(double amount) {
value.set(Double.doubleToLongBits(amount));
value = Double.doubleToLongBits(amount);
}

private static boolean isLessThan(double v1, double v2) {
return v1 < v2 || Double.isNaN(v2);
}

/** Set the current value to the maximum of the current value or the provided value. */
public void min(double v) {
if (Double.isFinite(v)) {
double min = get();
while (isLessThan(v, min) && !compareAndSet(min, v)) {
min = get();
}
}
}

private boolean isGreaterThan(double v1, double v2) {
private static boolean isGreaterThan(double v1, double v2) {
return v1 > v2 || Double.isNaN(v2);
}

Expand Down Expand Up @@ -124,4 +141,8 @@ public void max(double v) {
@Override public double doubleValue() {
return get();
}

@Override public String toString() {
return Double.toString(get());
}
}
128 changes: 111 additions & 17 deletions spectator-api/src/main/java/com/netflix/spectator/impl/StepDouble.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 Netflix, Inc.
* Copyright 2014-2024 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,7 +17,7 @@

import com.netflix.spectator.api.Clock;

import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;

/**
* Utility class for managing a set of AtomicLong instances mapped to a particular step interval.
Expand All @@ -35,41 +35,135 @@ public class StepDouble implements StepValue {
private final long step;

private volatile double previous;
private final AtomicDouble current;
private volatile long current;

private final AtomicLong lastInitPos;
private static final AtomicLongFieldUpdater<StepDouble> CURRENT_UPDATER =
AtomicLongFieldUpdater.newUpdater(StepDouble.class, "current");

private volatile long lastInitPos;

private static final AtomicLongFieldUpdater<StepDouble> LAST_INIT_POS_UPDATER =
AtomicLongFieldUpdater.newUpdater(StepDouble.class, "lastInitPos");

/** Create a new instance. */
public StepDouble(double init, Clock clock, long step) {
this.init = init;
this.clock = clock;
this.step = step;
previous = init;
current = new AtomicDouble(init);
lastInitPos = new AtomicLong(clock.wallTime() / step);
current = Double.doubleToLongBits(init);
lastInitPos = clock.wallTime() / step;
}

private void rollCount(long now) {
final long stepTime = now / step;
final long lastInit = lastInitPos.get();
if (lastInit < stepTime && lastInitPos.compareAndSet(lastInit, stepTime)) {
final double v = current.getAndSet(init);
final long lastInit = lastInitPos;
if (lastInit < stepTime && LAST_INIT_POS_UPDATER.compareAndSet(this, lastInit, stepTime)) {
final double v = Double.longBitsToDouble(
CURRENT_UPDATER.getAndSet(this, Double.doubleToLongBits(init)));
// Need to check if there was any activity during the previous step interval. If there was
// then the init position will move forward by 1, otherwise it will be older. No activity
// means the previous interval should be set to the `init` value.
previous = (lastInit == stepTime - 1) ? v : init;
}
}

/** Get the AtomicDouble for the current bucket. */
public AtomicDouble getCurrent() {
/** Get the value for the current bucket. */
public double getCurrent() {
return getCurrent(clock.wallTime());
}

/** Get the AtomicDouble for the current bucket. */
public AtomicDouble getCurrent(long now) {
/** Get the value for the current bucket. */
public double getCurrent(long now) {
rollCount(now);
return Double.longBitsToDouble(current);
}

/** Set the value for the current bucket. */
public void setCurrent(long now, double value) {
rollCount(now);
current = Double.doubleToLongBits(value);
}

/** Increment the current value and return the result. */
public double addAndGet(long now, double amount) {
rollCount(now);
return current;
long v;
double d;
double n;
long next;
do {
v = current;
d = Double.longBitsToDouble(v);
n = d + amount;
next = Double.doubleToLongBits(n);
} while (!CURRENT_UPDATER.compareAndSet(this, v, next));
return n;
}

/** Increment the current value and return the value before incrementing. */
public double getAndAdd(long now, double amount) {
rollCount(now);
long v;
double d;
double n;
long next;
do {
v = current;
d = Double.longBitsToDouble(v);
n = d + amount;
next = Double.doubleToLongBits(n);
} while (!CURRENT_UPDATER.compareAndSet(this, v, next));
return d;
}

/** Set the current value and return the previous value. */
public double getAndSet(long now, double value) {
rollCount(now);
long v = CURRENT_UPDATER.getAndSet(this, Double.doubleToLongBits(value));
return Double.longBitsToDouble(v);
}

private boolean compareAndSet(double expect, double update) {
long e = Double.doubleToLongBits(expect);
long u = Double.doubleToLongBits(update);
return CURRENT_UPDATER.compareAndSet(this, e, u);
}

/** Set the current value and return the previous value. */
public boolean compareAndSet(long now, double expect, double update) {
rollCount(now);
return compareAndSet(expect, update);
}

private static boolean isLessThan(double v1, double v2) {
return v1 < v2 || Double.isNaN(v2);
}

/** Set the current value to the minimum of the current value or the provided value. */
public void min(long now, double value) {
if (Double.isFinite(value)) {
rollCount(now);
double min = Double.longBitsToDouble(current);
while (isLessThan(value, min) && !compareAndSet(min, value)) {
min = Double.longBitsToDouble(current);
}
}
}

private static boolean isGreaterThan(double v1, double v2) {
return v1 > v2 || Double.isNaN(v2);
}

/** Set the current value to the maximum of the current value or the provided value. */
public void max(long now, double value) {
if (Double.isFinite(value)) {
rollCount(now);
double max = Double.longBitsToDouble(current);
while (isGreaterThan(value, max) && !compareAndSet(max, value)) {
max = Double.longBitsToDouble(current);
}
}
}

/** Get the value for the last completed interval. */
Expand Down Expand Up @@ -97,13 +191,13 @@ public double poll(long now) {

/** Get the timestamp for the end of the last completed interval. */
@Override public long timestamp() {
return lastInitPos.get() * step;
return lastInitPos * step;
}

@Override public String toString() {
return "StepDouble{init=" + init
+ ", previous=" + previous
+ ", current=" + current.get()
+ ", lastInitPos=" + lastInitPos.get() + '}';
+ ", current=" + Double.longBitsToDouble(current)
+ ", lastInitPos=" + lastInitPos + '}';
}
}
Loading

0 comments on commit 17e6d74

Please sign in to comment.