Skip to content

Commit 5dfd259

Browse files
authored
GH-1038: Trim object memory for ArrowBuf (#1044)
## What's Changed A significant number of ArrowBuf and BufferLedger objects are created during certain workloads. Saving several bytes per instance could add up to significant memory savings and reduced memory allocation expense and garbage collection. The id field, which was a sequential value used when logging object information, is replaced with an identity hash code. This should still allow enough information for debugging without the memory overhead. There may be possible duplicate values but it shouldn't matter for logging purposes. Atomic fields can be replaced by a primitive and a static updater which saves several bytes per instance. ### ArrowBuf | Component | Before | After | Savings | |-----------|--------|-------|---------| | `idGenerator` (static) | `AtomicLong` | Removed | 24 bytes globally | | `id` field (per instance) | `long` (8 bytes) | Removed | **8 bytes per instance** | | `getId()` | Returns `id` field | Returns `System.identityHashCode(this)` | — | ### BufferLedger | Component | Before | After | Savings | |-----------|--------|-------|---------| | `LEDGER_ID_GENERATOR` (static) | `AtomicLong` | Removed | 24 bytes globally | | `ledgerId` (per instance) | `long` (8 bytes) | Removed | **8 bytes per instance** | | `bufRefCnt` | `AtomicInteger` (24 bytes) | `volatile int` + static updater | **20 bytes per instance** | ### Total Savings | Scale | ArrowBuf | BufferLedger | Combined | |-------|----------|--------------|----------| | 100K | 800 KB | 2.8 MB | **~3.6 MB** | | 1M | 8 MB | 28 MB | **~36 MB** | | 10M | 80 MB | 280 MB | **~360 MB** | ### Benchmarking I ran the added benchmark before and after the metadata trimming. **Metadata Trimmed** | Benchmark | Mode | Score | Error |Units| |-------|----------|--------------|----------|----------| |MemoryFootprintBenchmarks.measureAllocationPerformance | avgt | 456.831 |± 36.059 | us/op| |MemoryFootprintBenchmarks.measureArrowBufMemoryFootprint | ss | 161.085 |± 35.596| ms/op| |Created 100000 ArrowBuf instances. Heap memory used | sum | 35631520 bytes (33.98 MB) |0 |bytes| |Average memory per ArrowBuf| sum | 356.32 bytes |0 |bytes| **Previous Object Layout** | Benchmark | Mode | Score | Error |Units| |-------|----------|--------------|----------|----------| |MemoryFootprintBenchmarks.measureAllocationPerformance | avgt | 466.171 |± 16.233 | us/op| |MemoryFootprintBenchmarks.measureArrowBufMemoryFootprint | ss | 176.790 |± 17.943 |ms/op| |Created 100000 ArrowBuf instances. Heap memory used | sum | 38817480 bytes (37.02 MB) |0 |bytes| |Average memory per ArrowBuf| sum | 388.17 bytes |0 |bytes| Closes #1038.
1 parent 41acbdc commit 5dfd259

File tree

4 files changed

+263
-39
lines changed

4 files changed

+263
-39
lines changed

memory/memory-core/src/main/java/org/apache/arrow/memory/Accountant.java

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*/
1717
package org.apache.arrow.memory;
1818

19-
import java.util.concurrent.atomic.AtomicLong;
19+
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
2020
import org.apache.arrow.util.Preconditions;
2121
import org.checkerframework.checker.nullness.qual.Nullable;
2222

@@ -37,16 +37,24 @@ class Accountant implements AutoCloseable {
3737
*/
3838
protected final long reservation;
3939

40-
private final AtomicLong peakAllocation = new AtomicLong();
40+
// AtomicLongFieldUpdaters for memory accounting fields to reduce memory overhead
41+
private static final AtomicLongFieldUpdater<Accountant> PEAK_ALLOCATION_UPDATER =
42+
AtomicLongFieldUpdater.newUpdater(Accountant.class, "peakAllocation");
43+
private static final AtomicLongFieldUpdater<Accountant> ALLOCATION_LIMIT_UPDATER =
44+
AtomicLongFieldUpdater.newUpdater(Accountant.class, "allocationLimit");
45+
private static final AtomicLongFieldUpdater<Accountant> LOCALLY_HELD_MEMORY_UPDATER =
46+
AtomicLongFieldUpdater.newUpdater(Accountant.class, "locallyHeldMemory");
47+
48+
private volatile long peakAllocation = 0;
4149

4250
/**
4351
* Maximum local memory that can be held. This can be externally updated. Changing it won't cause
4452
* past memory to change but will change responses to future allocation efforts
4553
*/
46-
private final AtomicLong allocationLimit = new AtomicLong();
54+
private volatile long allocationLimit = 0;
4755

4856
/** Currently allocated amount of memory. */
49-
private final AtomicLong locallyHeldMemory = new AtomicLong();
57+
private volatile long locallyHeldMemory = 0;
5058

5159
public Accountant(
5260
@Nullable Accountant parent, String name, long reservation, long maxAllocation) {
@@ -64,7 +72,7 @@ public Accountant(
6472
this.parent = parent;
6573
this.name = name;
6674
this.reservation = reservation;
67-
this.allocationLimit.set(maxAllocation);
75+
ALLOCATION_LIMIT_UPDATER.set(this, maxAllocation);
6876

6977
if (reservation != 0) {
7078
Preconditions.checkArgument(parent != null, "parent must not be null");
@@ -117,12 +125,12 @@ private AllocationOutcome.Status allocateBytesInternal(long size) {
117125
}
118126

119127
private void updatePeak() {
120-
final long currentMemory = locallyHeldMemory.get();
128+
final long currentMemory = locallyHeldMemory;
121129
while (true) {
122130

123-
final long previousPeak = peakAllocation.get();
131+
final long previousPeak = peakAllocation;
124132
if (currentMemory > previousPeak) {
125-
if (!peakAllocation.compareAndSet(previousPeak, currentMemory)) {
133+
if (!PEAK_ALLOCATION_UPDATER.compareAndSet(this, previousPeak, currentMemory)) {
126134
// peak allocation changed underneath us. try again.
127135
continue;
128136
}
@@ -166,15 +174,15 @@ private AllocationOutcome.Status allocate(
166174
final boolean incomingUpdatePeak,
167175
final boolean forceAllocation,
168176
@Nullable AllocationOutcomeDetails details) {
169-
final long oldLocal = locallyHeldMemory.getAndAdd(size);
177+
final long oldLocal = LOCALLY_HELD_MEMORY_UPDATER.getAndAdd(this, size);
170178
final long newLocal = oldLocal + size;
171179
// Borrowed from Math.addExact (but avoid exception here)
172180
// Overflow if result has opposite sign of both arguments
173181
// No need to reset locallyHeldMemory on overflow; allocateBytesInternal will releaseBytes on
174182
// failure
175183
final boolean overflow = ((oldLocal ^ newLocal) & (size ^ newLocal)) < 0;
176184
final long beyondReservation = newLocal - reservation;
177-
final boolean beyondLimit = overflow || newLocal > allocationLimit.get();
185+
final boolean beyondLimit = overflow || newLocal > allocationLimit;
178186
final boolean updatePeak = forceAllocation || (incomingUpdatePeak && !beyondLimit);
179187

180188
if (details != null) {
@@ -214,7 +222,7 @@ private AllocationOutcome.Status allocate(
214222

215223
public void releaseBytes(long size) {
216224
// reduce local memory. all memory released above reservation should be released up the tree.
217-
final long newSize = locallyHeldMemory.addAndGet(-size);
225+
final long newSize = LOCALLY_HELD_MEMORY_UPDATER.addAndGet(this, -size);
218226

219227
Preconditions.checkArgument(newSize >= 0, "Accounted size went negative.");
220228

@@ -255,7 +263,7 @@ public String getName() {
255263
* @return Limit in bytes.
256264
*/
257265
public long getLimit() {
258-
return allocationLimit.get();
266+
return allocationLimit;
259267
}
260268

261269
/**
@@ -274,7 +282,7 @@ public long getInitReservation() {
274282
* @param newLimit The limit in bytes.
275283
*/
276284
public void setLimit(long newLimit) {
277-
allocationLimit.set(newLimit);
285+
ALLOCATION_LIMIT_UPDATER.set(this, newLimit);
278286
}
279287

280288
/**
@@ -284,7 +292,7 @@ public void setLimit(long newLimit) {
284292
* @return Currently allocate memory in bytes.
285293
*/
286294
public long getAllocatedMemory() {
287-
return locallyHeldMemory.get();
295+
return locallyHeldMemory;
288296
}
289297

290298
/**
@@ -293,17 +301,17 @@ public long getAllocatedMemory() {
293301
* @return The peak allocated memory in bytes.
294302
*/
295303
public long getPeakMemoryAllocation() {
296-
return peakAllocation.get();
304+
return peakAllocation;
297305
}
298306

299307
public long getHeadroom() {
300-
long localHeadroom = allocationLimit.get() - locallyHeldMemory.get();
308+
long localHeadroom = allocationLimit - locallyHeldMemory;
301309
if (parent == null) {
302310
return localHeadroom;
303311
}
304312

305313
// Amount of reserved memory left on top of what parent has
306-
long reservedHeadroom = Math.max(0, reservation - locallyHeldMemory.get());
314+
long reservedHeadroom = Math.max(0, reservation - locallyHeldMemory);
307315
return Math.min(localHeadroom, parent.getHeadroom() + reservedHeadroom);
308316
}
309317
}

memory/memory-core/src/main/java/org/apache/arrow/memory/ArrowBuf.java

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import java.nio.ByteBuffer;
2525
import java.nio.ByteOrder;
2626
import java.nio.ReadOnlyBufferException;
27-
import java.util.concurrent.atomic.AtomicLong;
2827
import org.apache.arrow.memory.BaseAllocator.Verbosity;
2928
import org.apache.arrow.memory.util.CommonUtil;
3029
import org.apache.arrow.memory.util.HistoricalLog;
@@ -57,17 +56,17 @@ public final class ArrowBuf implements AutoCloseable {
5756
private static final int DOUBLE_SIZE = Double.BYTES;
5857
private static final int LONG_SIZE = Long.BYTES;
5958

60-
private static final AtomicLong idGenerator = new AtomicLong(0);
6159
private static final int LOG_BYTES_PER_ROW = 10;
62-
private final long id = idGenerator.incrementAndGet();
60+
6361
private final ReferenceManager referenceManager;
6462
private final @Nullable BufferManager bufferManager;
6563
private final long addr;
6664
private long readerIndex;
6765
private long writerIndex;
6866
private final @Nullable HistoricalLog historicalLog =
6967
BaseAllocator.DEBUG
70-
? new HistoricalLog(BaseAllocator.DEBUG_LOG_LENGTH, "ArrowBuf[%d]", id)
68+
? new HistoricalLog(
69+
BaseAllocator.DEBUG_LOG_LENGTH, "ArrowBuf[%d]", System.identityHashCode(this))
7170
: null;
7271
private volatile long capacity;
7372

@@ -218,7 +217,8 @@ public long memoryAddress() {
218217

219218
@Override
220219
public String toString() {
221-
return String.format("ArrowBuf[%d], address:%d, capacity:%d", id, memoryAddress(), capacity);
220+
return String.format(
221+
"ArrowBuf[%d], address:%d, capacity:%d", getId(), memoryAddress(), capacity);
222222
}
223223

224224
@Override
@@ -1080,12 +1080,15 @@ public String toHexString(final long start, final int length) {
10801080
}
10811081

10821082
/**
1083-
* Get the integer id assigned to this ArrowBuf for debugging purposes.
1083+
* Get the id assigned to this ArrowBuf for debugging purposes.
1084+
*
1085+
* <p>Returns {@link System#identityHashCode(Object)} which provides a unique identifier for this
1086+
* buffer without any per-instance memory overhead.
10841087
*
1085-
* @return integer id
1088+
* @return the identity hash code for this buffer
10861089
*/
10871090
public long getId() {
1088-
return id;
1091+
return System.identityHashCode(this);
10891092
}
10901093

10911094
/**

memory/memory-core/src/main/java/org/apache/arrow/memory/BufferLedger.java

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
package org.apache.arrow.memory;
1818

1919
import java.util.IdentityHashMap;
20-
import java.util.concurrent.atomic.AtomicInteger;
21-
import java.util.concurrent.atomic.AtomicLong;
20+
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
2221
import org.apache.arrow.memory.util.CommonUtil;
2322
import org.apache.arrow.memory.util.HistoricalLog;
2423
import org.apache.arrow.util.Preconditions;
@@ -32,12 +31,13 @@
3231
public class BufferLedger implements ValueWithKeyIncluded<BufferAllocator>, ReferenceManager {
3332
private final @Nullable IdentityHashMap<ArrowBuf, @Nullable Object> buffers =
3433
BaseAllocator.DEBUG ? new IdentityHashMap<>() : null;
35-
private static final AtomicLong LEDGER_ID_GENERATOR = new AtomicLong(0);
36-
// unique ID assigned to each ledger
37-
private final long ledgerId = LEDGER_ID_GENERATOR.incrementAndGet();
38-
private final AtomicInteger bufRefCnt = new AtomicInteger(0); // start at zero so we can
39-
// manage request for retain
40-
// correctly
34+
35+
// AtomicIntegerFieldUpdater for bufRefCnt to reduce memory overhead
36+
private static final AtomicIntegerFieldUpdater<BufferLedger> BUF_REF_CNT_UPDATER =
37+
AtomicIntegerFieldUpdater.newUpdater(BufferLedger.class, "bufRefCnt");
38+
// start at zero so we can manage request for retain correctly
39+
private volatile int bufRefCnt = 0;
40+
4141
private final long lCreationTime = System.nanoTime();
4242
private final BufferAllocator allocator;
4343
private final AllocationManager allocationManager;
@@ -78,15 +78,15 @@ public BufferAllocator getAllocator() {
7878
*/
7979
@Override
8080
public int getRefCount() {
81-
return bufRefCnt.get();
81+
return bufRefCnt;
8282
}
8383

8484
/**
8585
* Increment the ledger's reference count for the associated underlying memory chunk. All
8686
* ArrowBufs managed by this ledger will share the ref count.
8787
*/
8888
void increment() {
89-
bufRefCnt.incrementAndGet();
89+
BUF_REF_CNT_UPDATER.incrementAndGet(this);
9090
}
9191

9292
/**
@@ -144,7 +144,7 @@ private int decrement(int decrement) {
144144
allocator.assertOpen();
145145
final int outcome;
146146
synchronized (allocationManager) {
147-
outcome = bufRefCnt.addAndGet(-decrement);
147+
outcome = BUF_REF_CNT_UPDATER.addAndGet(this, -decrement);
148148
if (outcome == 0) {
149149
lDestructionTime = System.nanoTime();
150150
// refcount of this reference manager has dropped to 0
@@ -174,7 +174,7 @@ public void retain(int increment) {
174174
if (historicalLog != null) {
175175
historicalLog.recordEvent("retain(%d)", increment);
176176
}
177-
final int originalReferenceCount = bufRefCnt.getAndAdd(increment);
177+
final int originalReferenceCount = BUF_REF_CNT_UPDATER.getAndAdd(this, increment);
178178
Preconditions.checkArgument(originalReferenceCount > 0);
179179
}
180180

@@ -472,13 +472,13 @@ public long getAccountedSize() {
472472
void print(StringBuilder sb, int indent, BaseAllocator.Verbosity verbosity) {
473473
CommonUtil.indent(sb, indent)
474474
.append("ledger[")
475-
.append(ledgerId)
475+
.append(System.identityHashCode(this))
476476
.append("] allocator: ")
477477
.append(allocator.getName())
478478
.append("), isOwning: ")
479479
.append(", size: ")
480480
.append(", references: ")
481-
.append(bufRefCnt.get())
481+
.append(bufRefCnt)
482482
.append(", life: ")
483483
.append(lCreationTime)
484484
.append("..")

0 commit comments

Comments
 (0)