Skip to content

Commit 9b83dbd

Browse files
committed
[GR-52561] [GR-51456] [GR-52646] Implement C API symbol cache using an array.
PullRequest: graalpython/3236
2 parents e9e2ba2 + 484c30c commit 9b83dbd

File tree

16 files changed

+175
-207
lines changed

16 files changed

+175
-207
lines changed

graalpython/com.oracle.graal.python.test/src/com/oracle/graal/python/test/builtin/objects/cext/CExtContextTest.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -84,11 +84,6 @@ public TestCExtContext(PythonContext context, Object llvmLibrary) {
8484
super(context, llvmLibrary, false);
8585
}
8686

87-
@Override
88-
protected Store initializeSymbolCache() {
89-
throw new RuntimeException("should not reach here");
90-
}
91-
9287
public static TruffleString getBN(TruffleString s) {
9388
return getBaseName(s);
9489
}

graalpython/com.oracle.graal.python.test/src/tests/unittest_tags/test_concurrent_futures.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@
7373
*test.test_concurrent_futures.test_shutdown.ProcessPoolSpawnProcessPoolShutdownTest.test_processes_terminate
7474
*test.test_concurrent_futures.test_shutdown.ProcessPoolSpawnProcessPoolShutdownTest.test_run_after_shutdown
7575
*test.test_concurrent_futures.test_shutdown.ProcessPoolSpawnProcessPoolShutdownTest.test_shutdown_no_wait
76-
*test.test_concurrent_futures.test_shutdown.ProcessPoolSpawnProcessPoolShutdownTest.test_submit_after_interpreter_shutdown
7776
*test.test_concurrent_futures.test_shutdown.ThreadPoolShutdownTest.test_cancel_futures
7877
*test.test_concurrent_futures.test_shutdown.ThreadPoolShutdownTest.test_context_manager_shutdown
7978
*test.test_concurrent_futures.test_thread_pool.ThreadPoolExecutorTest.test_default_workers

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/PythonLanguage.java

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -364,9 +364,6 @@ public boolean isSingleContext() {
364364
@CompilationFinal(dimensions = 1) private volatile Object[] engineOptionsStorage;
365365
@CompilationFinal private volatile OptionValues engineOptions;
366366

367-
/** A shared shape for the C symbol cache (lazily initialized). */
368-
private Shape cApiSymbolCache;
369-
370367
/** For fast access to the PythonThreadState object by the owning thread. */
371368
private final ContextThreadLocal<PythonThreadState> threadState = locals.createContextThreadLocal(PythonContext.PythonThreadState::new);
372369

@@ -984,17 +981,6 @@ public Shape getBuiltinTypeInstanceShape(PythonBuiltinClassType type) {
984981
return shape;
985982
}
986983

987-
/**
988-
* Returns the shape used for the C API symbol cache.
989-
*/
990-
@TruffleBoundary
991-
public synchronized Shape getCApiSymbolCacheShape() {
992-
if (cApiSymbolCache == null) {
993-
cApiSymbolCache = Shape.newBuilder().build();
994-
}
995-
return cApiSymbolCache;
996-
}
997-
998984
/**
999985
* Cache call targets that are created for every new context, based on a single key.
1000986
*/

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextTypeBuiltins.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@
7979
import com.oracle.graal.python.builtins.objects.cext.capi.ExternalFunctionNodes.SetterRoot;
8080
import com.oracle.graal.python.builtins.objects.cext.common.CArrayWrappers.CArrayWrapper;
8181
import com.oracle.graal.python.builtins.objects.cext.common.CExtContext;
82-
import com.oracle.graal.python.builtins.objects.cext.common.CExtContext.Store;
8382
import com.oracle.graal.python.builtins.objects.common.DynamicObjectStorage;
8483
import com.oracle.graal.python.builtins.objects.dict.PDict;
8584
import com.oracle.graal.python.builtins.objects.function.PBuiltinFunction;
@@ -120,6 +119,8 @@
120119
import com.oracle.truffle.api.library.CachedLibrary;
121120
import com.oracle.truffle.api.nodes.Node;
122121
import com.oracle.truffle.api.nodes.RootNode;
122+
import com.oracle.truffle.api.object.DynamicObject;
123+
import com.oracle.truffle.api.object.Shape;
123124
import com.oracle.truffle.api.profiles.InlinedExactClassProfile;
124125
import com.oracle.truffle.api.strings.TruffleString;
125126
import com.oracle.truffle.api.utilities.CyclicAssumption;
@@ -164,14 +165,20 @@ static Object doIt(PythonNativeClass self, TruffleString className) {
164165
}
165166
}
166167

168+
static final class NativeTypeDictStorage extends DynamicObject {
169+
public NativeTypeDictStorage(Shape shape) {
170+
super(shape);
171+
}
172+
}
173+
167174
@CApiBuiltin(ret = PyObjectTransfer, args = {PyTypeObject}, call = Ignored)
168175
abstract static class PyTruffle_NewTypeDict extends CApiUnaryBuiltinNode {
169176

170177
@Specialization
171178
@TruffleBoundary
172179
static PDict doGeneric(PythonNativeClass nativeClass) {
173180
PythonLanguage language = PythonLanguage.get(null);
174-
Store nativeTypeStore = new Store(language.getEmptyShape());
181+
NativeTypeDictStorage nativeTypeStore = new NativeTypeDictStorage(language.getEmptyShape());
175182
PDict dict = PythonObjectFactory.getUncached().createDict(new DynamicObjectStorage(nativeTypeStore));
176183
HiddenAttr.WriteNode.executeUncached(dict, HiddenAttr.INSTANCESHAPE, language.getShapeForClass(nativeClass));
177184
return dict;

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CApiContext.java

Lines changed: 102 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848

4949
import java.io.IOException;
5050
import java.io.PrintStream;
51+
import java.lang.invoke.VarHandle;
5152
import java.nio.file.LinkOption;
5253
import java.util.ArrayList;
5354
import java.util.Arrays;
@@ -62,6 +63,7 @@
6263

6364
import org.graalvm.collections.EconomicMap;
6465
import org.graalvm.collections.Pair;
66+
import org.graalvm.nativeimage.ImageInfo;
6567

6668
import com.oracle.graal.python.PythonLanguage;
6769
import com.oracle.graal.python.builtins.PythonBuiltinClassType;
@@ -126,8 +128,6 @@
126128
import com.oracle.truffle.api.library.ExportLibrary;
127129
import com.oracle.truffle.api.library.ExportMessage;
128130
import com.oracle.truffle.api.nodes.Node;
129-
import com.oracle.truffle.api.object.DynamicObjectLibrary;
130-
import com.oracle.truffle.api.object.Shape;
131131
import com.oracle.truffle.api.source.Source;
132132
import com.oracle.truffle.api.source.Source.SourceBuilder;
133133
import com.oracle.truffle.api.strings.TruffleString;
@@ -201,6 +201,18 @@ public final class CApiContext extends CExtContext {
201201
public Object timezoneType;
202202
private PyCapsule pyDateTimeCAPICapsule;
203203

204+
/**
205+
* Same as {@link #nativeSymbolCache} if there is only one context per JVM (i.e. just one engine
206+
* in single-context mode). Will be {@code null} in case of multiple contexts.
207+
*/
208+
@CompilationFinal(dimensions = 1) private static Object[] nativeSymbolCacheSingleContext;
209+
private static boolean nativeSymbolCacheSingleContextUsed;
210+
211+
/**
212+
* A private (i.e. per-context) cache of C API symbols (usually helper functions).
213+
*/
214+
private final Object[] nativeSymbolCache;
215+
204216
private record ClosureInfo(Object closure, Object delegate, Object executable, long pointer) {
205217
}
206218

@@ -239,6 +251,37 @@ public static TruffleLogger getLogger(Class<?> clazz) {
239251

240252
public CApiContext(PythonContext context, Object llvmLibrary, boolean useNativeBackend) {
241253
super(context, llvmLibrary, useNativeBackend);
254+
this.nativeSymbolCache = new Object[NativeCAPISymbol.values().length];
255+
256+
/*
257+
* Publish the native symbol cache to the static field if following is given: (1) The static
258+
* field hasn't been used by another instance yet (i.e. '!used'), and (2) we are in
259+
* single-context mode. This initialization ensures that if
260+
* 'CApiContext.nativeSymbolCacheSingleContext != null', the context is safe to use it and
261+
* just needs to do a null check.
262+
*/
263+
synchronized (CApiContext.class) {
264+
if (!CApiContext.nativeSymbolCacheSingleContextUsed && context.getLanguage().isSingleContext()) {
265+
assert CApiContext.nativeSymbolCacheSingleContext == null;
266+
267+
// we cannot be in built-time code because this is using pre-initialized contexts
268+
assert !ImageInfo.inImageBuildtimeCode();
269+
270+
// this is the first context accessing the static symbol cache
271+
CApiContext.nativeSymbolCacheSingleContext = this.nativeSymbolCache;
272+
} else if (CApiContext.nativeSymbolCacheSingleContext != null) {
273+
assert CApiContext.nativeSymbolCacheSingleContextUsed;
274+
/*
275+
* In this case, this context instance is at least the second one attempting to use
276+
* the static symbol cache. We now clear the static field to indicate that every
277+
* context instance should use its private cache. If a former context already used
278+
* the cache and there is already compiled code, it is not necessary to invalidate
279+
* the code because the cache is still valid.
280+
*/
281+
CApiContext.nativeSymbolCacheSingleContext = null;
282+
}
283+
CApiContext.nativeSymbolCacheSingleContextUsed = true;
284+
}
242285

243286
// initialize primitive native wrapper cache
244287
primitiveNativeWrapperCache = new PrimitiveNativeWrapper[262];
@@ -382,27 +425,59 @@ private void freeSmallInts() {
382425
}
383426
}
384427

385-
@TruffleBoundary
386-
@Override
387-
protected Store initializeSymbolCache() {
388-
PythonLanguage language = getContext().getLanguage();
389-
Shape symbolCacheShape = language.getCApiSymbolCacheShape();
390-
// We will always get an empty shape from the language and we do always add same key-value
391-
// pairs (in the same order). So, in the end, each context should get the same shape.
392-
Store s = new Store(symbolCacheShape);
393-
for (NativeCAPISymbol sym : NativeCAPISymbol.getValues()) {
394-
DynamicObjectLibrary.getUncached().put(s, sym, PNone.NO_VALUE);
395-
}
396-
return s;
397-
}
398-
399428
public Object getModuleByIndex(int i) {
400429
if (i < modulesByIndex.size()) {
401430
return modulesByIndex.get(i);
402431
}
403432
return null;
404433
}
405434

435+
/**
436+
* Retrieves the C API symbol cache instance in the fastest possible way. If there is just one
437+
* instance of {@link CApiContext}, it will load the cache stored from the static field
438+
* {@link CApiContext#nativeSymbolCacheSingleContext}. Otherwise, it will load the cache from
439+
* the instance field {@link CApiContext#nativeSymbolCache}.
440+
*
441+
* @param caller The requesting node (may be {@code null}). Used for the fast-path lookup of the
442+
* {@link CApiContext} instance (if necessary).
443+
* @return The C API symbol cache.
444+
*/
445+
private static Object[] getSymbolCache(Node caller) {
446+
Object[] nativeSymbolCacheSingleContext = CApiContext.nativeSymbolCacheSingleContext;
447+
if (nativeSymbolCacheSingleContext != null) {
448+
return nativeSymbolCacheSingleContext;
449+
}
450+
return PythonContext.get(caller).getCApiContext().nativeSymbolCache;
451+
}
452+
453+
public static Object getNativeSymbol(Node caller, NativeCAPISymbol symbol) {
454+
Object[] nativeSymbolCache = getSymbolCache(caller);
455+
Object result = nativeSymbolCache[symbol.ordinal()];
456+
if (result == null) {
457+
CompilerDirectives.transferToInterpreterAndInvalidate();
458+
result = lookupNativeSymbol(nativeSymbolCache, symbol);
459+
}
460+
assert result != null;
461+
return result;
462+
}
463+
464+
/**
465+
* Lookup the given C API symbol in the library, store it to the provided cache, and return the
466+
* callable symbol.
467+
*/
468+
private static Object lookupNativeSymbol(Object[] nativeSymbolCache, NativeCAPISymbol symbol) {
469+
CompilerAsserts.neverPartOfCompilation();
470+
String name = symbol.getName();
471+
try {
472+
Object nativeSymbol = InteropLibrary.getUncached().readMember(PythonContext.get(null).getCApiContext().getLLVMLibrary(), name);
473+
nativeSymbol = CExtContext.ensureExecutable(nativeSymbol, symbol);
474+
VarHandle.storeStoreFence();
475+
return nativeSymbolCache[symbol.ordinal()] = nativeSymbol;
476+
} catch (UnsupportedMessageException | UnknownIdentifierException e) {
477+
throw CompilerDirectives.shouldNotReachHere(e);
478+
}
479+
}
480+
406481
@TruffleBoundary
407482
public AllocInfo traceFree(Object ptr, @SuppressWarnings("unused") PFrame.Reference curFrame, @SuppressWarnings("unused") TruffleString clazzName) {
408483
if (allocatedNativeMemory == null) {
@@ -746,6 +821,17 @@ public void finalizeCapi() {
746821
for (Object pyMethodDefPointer : methodDefinitions.values()) {
747822
PyMethodDefHelper.free(pyMethodDefPointer);
748823
}
824+
/*
825+
* If the static symbol cache is not null, then it is guaranteed that this context instance
826+
* was the exclusive user of it. We can now reset the state such that other contexts created
827+
* after this can use it.
828+
*/
829+
synchronized (CApiContext.class) {
830+
if (CApiContext.nativeSymbolCacheSingleContext != null) {
831+
CApiContext.nativeSymbolCacheSingleContext = null;
832+
CApiContext.nativeSymbolCacheSingleContextUsed = false;
833+
}
834+
}
749835
}
750836

751837
@TruffleBoundary

0 commit comments

Comments
 (0)