Skip to content

Commit 8cc1304

Browse files
mernst-githubJornVernee
authored andcommitted
8287788: Implement a better allocator for downcalls
Reviewed-by: jvernee
1 parent 039e73f commit 8cc1304

File tree

7 files changed

+474
-20
lines changed

7 files changed

+474
-20
lines changed

src/java.base/share/classes/jdk/internal/foreign/SlicingAllocator.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -38,6 +38,22 @@ public SlicingAllocator(MemorySegment segment) {
3838
this.segment = segment;
3939
}
4040

41+
public long currentOffset() {
42+
return sp;
43+
}
44+
45+
public void resetTo(long offset) {
46+
if (offset < 0 || offset > sp)
47+
throw new IllegalArgumentException(String.format("offset %d should be in [0, %d] ", offset, sp));
48+
this.sp = offset;
49+
}
50+
51+
public boolean canAllocate(long byteSize, long byteAlignment) {
52+
long min = segment.address();
53+
long start = Utils.alignUp(min + sp, byteAlignment) - min;
54+
return start + byteSize <= segment.byteSize();
55+
}
56+
4157
MemorySegment trySlice(long byteSize, long byteAlignment) {
4258
long min = segment.address();
4359
long start = Utils.alignUp(min + sp, byteAlignment) - min;
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation. Oracle designates this
8+
* particular file as subject to the "Classpath" exception as provided
9+
* by Oracle in the LICENSE file that accompanied this code.
10+
*
11+
* This code is distributed in the hope that it will be useful, but WITHOUT
12+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14+
* version 2 for more details (a copy is included in the LICENSE file that
15+
* accompanied this code).
16+
*
17+
* You should have received a copy of the GNU General Public License version
18+
* 2 along with this work; if not, write to the Free Software Foundation,
19+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20+
*
21+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22+
* or visit www.oracle.com if you need additional information or have any
23+
* questions.
24+
*/
25+
package jdk.internal.foreign.abi;
26+
27+
import jdk.internal.foreign.SlicingAllocator;
28+
import jdk.internal.misc.CarrierThreadLocal;
29+
import jdk.internal.vm.annotation.ForceInline;
30+
31+
import java.lang.foreign.Arena;
32+
import java.lang.foreign.MemorySegment;
33+
import java.lang.foreign.SegmentAllocator;
34+
import java.util.concurrent.locks.ReentrantLock;
35+
36+
public class BufferStack {
37+
private final long size;
38+
39+
public BufferStack(long size) {
40+
this.size = size;
41+
}
42+
43+
private final ThreadLocal<PerThread> tl = new CarrierThreadLocal<>() {
44+
@Override
45+
protected PerThread initialValue() {
46+
return new PerThread(size);
47+
}
48+
};
49+
50+
@ForceInline
51+
public Arena pushFrame(long size, long byteAlignment) {
52+
return tl.get().pushFrame(size, byteAlignment);
53+
}
54+
55+
private static final class PerThread {
56+
private final ReentrantLock lock = new ReentrantLock();
57+
private final SlicingAllocator stack;
58+
59+
public PerThread(long size) {
60+
this.stack = new SlicingAllocator(Arena.ofAuto().allocate(size));
61+
}
62+
63+
@ForceInline
64+
public Arena pushFrame(long size, long byteAlignment) {
65+
boolean needsLock = Thread.currentThread().isVirtual() && !lock.isHeldByCurrentThread();
66+
if (needsLock && !lock.tryLock()) {
67+
// Rare: another virtual thread on the same carrier competed for acquisition.
68+
return Arena.ofConfined();
69+
}
70+
if (!stack.canAllocate(size, byteAlignment)) {
71+
if (needsLock) lock.unlock();
72+
return Arena.ofConfined();
73+
}
74+
75+
return new Frame(needsLock, size, byteAlignment);
76+
}
77+
78+
private class Frame implements Arena {
79+
private final boolean locked;
80+
private final long parentOffset;
81+
private final long topOfStack;
82+
private final Arena scope = Arena.ofConfined();
83+
private final SegmentAllocator frame;
84+
85+
@SuppressWarnings("restricted")
86+
public Frame(boolean locked, long byteSize, long byteAlignment) {
87+
this.locked = locked;
88+
89+
parentOffset = stack.currentOffset();
90+
MemorySegment frameSegment = stack.allocate(byteSize, byteAlignment);
91+
topOfStack = stack.currentOffset();
92+
frame = new SlicingAllocator(frameSegment.reinterpret(scope, null));
93+
}
94+
95+
private void assertOrder() {
96+
if (topOfStack != stack.currentOffset())
97+
throw new IllegalStateException("Out of order access: frame not top-of-stack");
98+
}
99+
100+
@Override
101+
@SuppressWarnings("restricted")
102+
public MemorySegment allocate(long byteSize, long byteAlignment) {
103+
return frame.allocate(byteSize, byteAlignment);
104+
}
105+
106+
@Override
107+
public MemorySegment.Scope scope() {
108+
return scope.scope();
109+
}
110+
111+
@Override
112+
public void close() {
113+
assertOrder();
114+
scope.close();
115+
stack.resetTo(parentOffset);
116+
if (locked) {
117+
lock.unlock();
118+
}
119+
}
120+
}
121+
}
122+
}

src/java.base/share/classes/jdk/internal/foreign/abi/SharedUtils.java

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -382,26 +382,12 @@ static long pickChunkOffset(long chunkOffset, long byteWidth, int chunkWidth) {
382382
: chunkOffset;
383383
}
384384

385-
public static Arena newBoundedArena(long size) {
386-
return new Arena() {
387-
final Arena arena = Arena.ofConfined();
388-
final SegmentAllocator slicingAllocator = SegmentAllocator.slicingAllocator(arena.allocate(size));
389-
390-
@Override
391-
public Scope scope() {
392-
return arena.scope();
393-
}
385+
private static final int LINKER_STACK_SIZE = Integer.getInteger("jdk.internal.foreign.LINKER_STACK_SIZE", 256);
386+
private static final BufferStack LINKER_STACK = new BufferStack(LINKER_STACK_SIZE);
394387

395-
@Override
396-
public void close() {
397-
arena.close();
398-
}
399-
400-
@Override
401-
public MemorySegment allocate(long byteSize, long byteAlignment) {
402-
return slicingAllocator.allocate(byteSize, byteAlignment);
403-
}
404-
};
388+
@ForceInline
389+
public static Arena newBoundedArena(long size) {
390+
return LINKER_STACK.pushFrame(size, 8);
405391
}
406392

407393
public static Arena newEmptyArena() {
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
/*
25+
* @test
26+
* @modules java.base/jdk.internal.foreign.abi
27+
* @build NativeTestHelper TestBufferStack
28+
* @run testng/othervm --enable-native-access=ALL-UNNAMED TestBufferStack
29+
*/
30+
31+
import jdk.internal.foreign.abi.BufferStack;
32+
import org.testng.Assert;
33+
import org.testng.annotations.Test;
34+
35+
import java.lang.foreign.Arena;
36+
import java.lang.foreign.FunctionDescriptor;
37+
import java.lang.foreign.MemoryLayout;
38+
import java.lang.foreign.MemorySegment;
39+
import java.lang.foreign.SegmentAllocator;
40+
import java.lang.invoke.MethodHandle;
41+
import java.time.Duration;
42+
import java.util.Arrays;
43+
import java.util.stream.IntStream;
44+
45+
import static java.lang.foreign.MemoryLayout.structLayout;
46+
import static java.lang.foreign.ValueLayout.*;
47+
import static java.time.temporal.ChronoUnit.SECONDS;
48+
49+
public class TestBufferStack extends NativeTestHelper {
50+
@Test
51+
public void testScopedAllocation() {
52+
int stackSize = 128;
53+
BufferStack stack = new BufferStack(stackSize);
54+
MemorySegment stackSegment;
55+
try (Arena frame1 = stack.pushFrame(3 * JAVA_INT.byteSize(), JAVA_INT.byteAlignment())) {
56+
// Segments have expected sizes and are accessible and allocated consecutively in the same scope.
57+
MemorySegment segment11 = frame1.allocate(JAVA_INT);
58+
Assert.assertEquals(segment11.scope(), frame1.scope());
59+
Assert.assertEquals(segment11.byteSize(), JAVA_INT.byteSize());
60+
segment11.set(JAVA_INT, 0, 1);
61+
stackSegment = segment11.reinterpret(stackSize);
62+
63+
MemorySegment segment12 = frame1.allocate(JAVA_INT);
64+
Assert.assertEquals(segment12.address(), segment11.address() + JAVA_INT.byteSize());
65+
Assert.assertEquals(segment12.byteSize(), JAVA_INT.byteSize());
66+
Assert.assertEquals(segment12.scope(), frame1.scope());
67+
segment12.set(JAVA_INT, 0, 1);
68+
69+
MemorySegment segment2;
70+
try (Arena frame2 = stack.pushFrame(JAVA_LONG.byteSize(), JAVA_LONG.byteAlignment())) {
71+
Assert.assertNotEquals(frame2.scope(), frame1.scope());
72+
// same here, but a new scope.
73+
segment2 = frame2.allocate(JAVA_LONG);
74+
Assert.assertEquals(segment2.address(), segment12.address() + /*segment12 size + frame 1 spare + alignment constraint*/ 3 * JAVA_INT.byteSize());
75+
Assert.assertEquals(segment2.byteSize(), JAVA_LONG.byteSize());
76+
Assert.assertEquals(segment2.scope(), frame2.scope());
77+
segment2.set(JAVA_LONG, 0, 1);
78+
79+
// Frames must be closed in stack order.
80+
Assert.assertThrows(IllegalStateException.class, frame1::close);
81+
}
82+
// Scope is closed here, inner segments throw.
83+
Assert.assertThrows(IllegalStateException.class, () -> segment2.get(JAVA_INT, 0));
84+
// A new stack frame allocates at the same location (but different scope) as the previous did.
85+
try (Arena frame3 = stack.pushFrame(2 * JAVA_INT.byteSize(), JAVA_INT.byteAlignment())) {
86+
MemorySegment segment3 = frame3.allocate(JAVA_INT);
87+
Assert.assertEquals(segment3.scope(), frame3.scope());
88+
Assert.assertEquals(segment3.address(), segment12.address() + 2 * JAVA_INT.byteSize());
89+
}
90+
91+
// Fallback arena behaves like regular stack frame.
92+
MemorySegment outOfStack;
93+
try (Arena hugeFrame = stack.pushFrame(1024, 4)) {
94+
outOfStack = hugeFrame.allocate(4);
95+
Assert.assertEquals(outOfStack.scope(), hugeFrame.scope());
96+
Assert.assertTrue(outOfStack.asOverlappingSlice(stackSegment).isEmpty());
97+
}
98+
Assert.assertThrows(IllegalStateException.class, () -> outOfStack.get(JAVA_INT, 0));
99+
100+
// Outer segments are still accessible.
101+
segment11.get(JAVA_INT, 0);
102+
segment12.get(JAVA_INT, 0);
103+
}
104+
}
105+
106+
@Test
107+
public void stress() throws InterruptedException {
108+
BufferStack stack = new BufferStack(256);
109+
Thread[] vThreads = IntStream.range(0, 1024).mapToObj(_ ->
110+
Thread.ofVirtual().start(() -> {
111+
long threadId = Thread.currentThread().threadId();
112+
while (!Thread.interrupted()) {
113+
for (int i = 0; i < 1_000_000; i++) {
114+
try (Arena arena = stack.pushFrame(JAVA_LONG.byteSize(), JAVA_LONG.byteAlignment())) {
115+
// Try to assert no two vThreads get allocated the same stack space.
116+
MemorySegment segment = arena.allocate(JAVA_LONG);
117+
JAVA_LONG.varHandle().setVolatile(segment, 0L, threadId);
118+
Assert.assertEquals(threadId, (long) JAVA_LONG.varHandle().getVolatile(segment, 0L));
119+
}
120+
}
121+
Thread.yield(); // make sure the driver thread gets a chance.
122+
}
123+
})).toArray(Thread[]::new);
124+
Thread.sleep(Duration.of(10, SECONDS));
125+
Arrays.stream(vThreads).forEach(
126+
thread -> {
127+
Assert.assertTrue(thread.isAlive());
128+
thread.interrupt();
129+
});
130+
}
131+
132+
static {
133+
System.loadLibrary("TestBufferStack");
134+
}
135+
136+
private static final MemoryLayout HVAPoint3D = structLayout(NativeTestHelper.C_DOUBLE, C_DOUBLE, C_DOUBLE);
137+
private static final MemorySegment UPCALL_MH = upcallStub(TestBufferStack.class, "recurse", FunctionDescriptor.of(HVAPoint3D, C_INT));
138+
private static final MethodHandle DOWNCALL_MH = downcallHandle("recurse", FunctionDescriptor.of(HVAPoint3D, C_INT, ADDRESS));
139+
140+
public static MemorySegment recurse(int depth) {
141+
try {
142+
return (MemorySegment) DOWNCALL_MH.invokeExact((SegmentAllocator) Arena.ofAuto(), depth, UPCALL_MH);
143+
} catch (Throwable e) {
144+
throw new RuntimeException(e);
145+
}
146+
}
147+
148+
@Test
149+
public void testDeepStack() throws Throwable {
150+
// Each downcall and upcall require 48 bytes of stack.
151+
// After five allocations we start falling back.
152+
MemorySegment point = recurse(10);
153+
Assert.assertEquals(point.getAtIndex(C_DOUBLE, 0), 12.0);
154+
Assert.assertEquals(point.getAtIndex(C_DOUBLE, 1), 11.0);
155+
Assert.assertEquals(point.getAtIndex(C_DOUBLE, 2), 10.0);
156+
}
157+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
#include "export.h"
25+
26+
typedef struct { double x, y, z; } HVAPoint3D;
27+
28+
EXPORT HVAPoint3D recurse(int depth, HVAPoint3D (*cb)(int)) {
29+
if (depth == 0) {
30+
HVAPoint3D result = { 2, 1, 0};
31+
return result;
32+
}
33+
34+
HVAPoint3D result = cb(depth - 1);
35+
result.x += 1;
36+
result.y += 1;
37+
result.z += 1;
38+
return result;
39+
}

0 commit comments

Comments
 (0)