Skip to content

Commit 8240e1d

Browse files
committed
parallel compilation wip - use virtual threads
1 parent 4691f7b commit 8240e1d

File tree

3 files changed

+123
-99
lines changed

3 files changed

+123
-99
lines changed

MILESTONES.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ The following areas are currently under active development to enhance the functi
230230
- Added `Time::HiRes`, `Benchmark` modules.
231231
- Added `/ee` regex modifier.
232232
- Added no strict `vars`, `subs`.
233-
- A small part of the code generation executes in a separate thread, for faster module loading.
233+
- A small part of the code generation executes in a virtual thread, for faster module loading.
234234
- Use `int` instead of `enum` to reduce the memory overhead of scalar variables.
235235
- Planned release date: 2025-04-10
236236

src/main/java/org/perlonjava/parser/SubroutineParser.java

+113-95
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
import java.util.ArrayList;
1414
import java.util.List;
1515
import java.util.Map;
16+
import java.util.concurrent.ExecutorService;
17+
import java.util.concurrent.Executors;
18+
import java.util.concurrent.Future;
1619
import java.util.concurrent.Semaphore;
1720

1821
import static org.perlonjava.parser.PrototypeArgs.consumeArgsWithPrototype;
@@ -185,74 +188,87 @@ public static Node parseSubroutineDefinition(Parser parser, boolean wantName, St
185188
if (subName == null) {
186189
return handleAnonSub(parser, subName, prototype, attributes, block, currentIndex);
187190
} else {
188-
// - register the subroutine in the namespace
189-
String fullName = NameNormalizer.normalizeVariableName(subName, parser.ctx.symbolTable.getCurrentPackage());
190-
RuntimeCode code = (RuntimeCode) GlobalVariable.getGlobalCodeRef(fullName).value;
191-
code.prototype = prototype;
192-
code.attributes = attributes;
193-
194-
// Optimization - https://github.com/fglock/PerlOnJava/issues/8
195-
// Prepare capture variables
196-
Map<Integer, SymbolTable.SymbolEntry> outerVars = parser.ctx.symbolTable.getAllVisibleVariables();
197-
ArrayList<Class> classList = new ArrayList<>();
198-
ArrayList<Object> paramList = new ArrayList<>();
199-
for (SymbolTable.SymbolEntry entry : outerVars.values()) {
200-
if (!entry.name().equals("@_") && !entry.decl().isEmpty()) {
201-
String sigil = entry.name().substring(0, 1);
202-
String variableName = null;
203-
if (entry.decl().equals("our")) {
204-
variableName = NameNormalizer.normalizeVariableName(
205-
entry.name().substring(1),
206-
entry.perlPackage());
207-
} else {
208-
// "my" or "state" variable live in a special BEGIN package
209-
// Retrieve the variable id from the AST; create a new id if needed
210-
OperatorNode ast = entry.ast();
211-
if (ast.id == 0) {
212-
ast.id = EmitterMethodCreator.classCounter++;
213-
}
214-
variableName = NameNormalizer.normalizeVariableName(
215-
entry.name().substring(1),
216-
PersistentVariable.beginPackage(ast.id));
191+
return handleNamedSub(parser, subName, prototype, attributes, block);
192+
}
193+
}
194+
195+
private static ListNode handleNamedSub(Parser parser, String subName, String prototype, List<String> attributes, BlockNode block) {
196+
// - register the subroutine in the namespace
197+
String fullName = NameNormalizer.normalizeVariableName(subName, parser.ctx.symbolTable.getCurrentPackage());
198+
RuntimeCode code = (RuntimeCode) GlobalVariable.getGlobalCodeRef(fullName).value;
199+
code.prototype = prototype;
200+
code.attributes = attributes;
201+
202+
// Optimization - https://github.com/fglock/PerlOnJava/issues/8
203+
// Prepare capture variables
204+
Map<Integer, SymbolTable.SymbolEntry> outerVars = parser.ctx.symbolTable.getAllVisibleVariables();
205+
ArrayList<Class> classList = new ArrayList<>();
206+
ArrayList<Object> paramList = new ArrayList<>();
207+
for (SymbolTable.SymbolEntry entry : outerVars.values()) {
208+
if (!entry.name().equals("@_") && !entry.decl().isEmpty()) {
209+
String sigil = entry.name().substring(0, 1);
210+
String variableName = null;
211+
if (entry.decl().equals("our")) {
212+
// Normalize variable name for 'our' declarations
213+
variableName = NameNormalizer.normalizeVariableName(
214+
entry.name().substring(1),
215+
entry.perlPackage());
216+
} else {
217+
// Handle "my" or "state" variables which live in a special BEGIN package
218+
// Retrieve the variable id from the AST; create a new id if needed
219+
OperatorNode ast = entry.ast();
220+
if (ast.id == 0) {
221+
ast.id = EmitterMethodCreator.classCounter++;
217222
}
218-
classList.add(
219-
switch (sigil) {
220-
case "$" -> RuntimeScalar.class;
221-
case "%" -> RuntimeHash.class;
222-
case "@" -> RuntimeArray.class;
223-
default -> throw new IllegalStateException("Unexpected value: " + sigil);
224-
}
225-
);
226-
paramList.add(
227-
switch (sigil) {
228-
case "$" -> GlobalVariable.getGlobalVariable(variableName);
229-
case "%" -> GlobalVariable.getGlobalHash(variableName);
230-
case "@" -> GlobalVariable.getGlobalArray(variableName);
231-
default -> throw new IllegalStateException("Unexpected value: " + sigil);
232-
}
233-
);
234-
// System.out.println("Capture " + entry.decl() + " " + entry.name() + " as " + variableName);
223+
// Normalize variable name for 'my' or 'state' declarations
224+
variableName = NameNormalizer.normalizeVariableName(
225+
entry.name().substring(1),
226+
PersistentVariable.beginPackage(ast.id));
235227
}
228+
// Determine the class type based on the sigil
229+
classList.add(
230+
switch (sigil) {
231+
case "$" -> RuntimeScalar.class;
232+
case "%" -> RuntimeHash.class;
233+
case "@" -> RuntimeArray.class;
234+
default -> throw new IllegalStateException("Unexpected value: " + sigil);
235+
}
236+
);
237+
// Add the corresponding global variable to the parameter list
238+
paramList.add(
239+
switch (sigil) {
240+
case "$" -> GlobalVariable.getGlobalVariable(variableName);
241+
case "%" -> GlobalVariable.getGlobalHash(variableName);
242+
case "@" -> GlobalVariable.getGlobalArray(variableName);
243+
default -> throw new IllegalStateException("Unexpected value: " + sigil);
244+
}
245+
);
246+
// System.out.println("Capture " + entry.decl() + " " + entry.name() + " as " + variableName);
236247
}
237-
EmitterContext newCtx = new EmitterContext(
238-
new JavaClassInfo(),
239-
parser.ctx.symbolTable.snapShot(),
240-
null,
241-
null,
242-
RuntimeContextType.RUNTIME,
243-
true,
244-
parser.ctx.errorUtil,
245-
parser.ctx.compilerOptions,
246-
new RuntimeArray()
247-
);
248-
249-
byte[] classData = EmitterMethodCreator.getBytecode(newCtx, block, false);
250-
// System.out.println("Creating subroutine " + fullName);
251-
Class<?> generatedClass = EmitterMethodCreator.loadBytecode(newCtx, classData);
252-
253-
// Create a Runnable to execute the subroutine creation
254-
Runnable subroutineCreationTask = () -> {
255-
248+
}
249+
// Create a new EmitterContext for generating bytecode
250+
EmitterContext newCtx = new EmitterContext(
251+
new JavaClassInfo(),
252+
parser.ctx.symbolTable.snapShot(),
253+
null,
254+
null,
255+
RuntimeContextType.RUNTIME,
256+
true,
257+
parser.ctx.errorUtil,
258+
parser.ctx.compilerOptions,
259+
new RuntimeArray()
260+
);
261+
262+
// Generate bytecode for the subroutine block
263+
byte[] classData = EmitterMethodCreator.getBytecode(newCtx, block, false);
264+
// System.out.println("Creating subroutine " + fullName);
265+
// Load the generated bytecode into a Class object
266+
Class<?> generatedClass = EmitterMethodCreator.loadBytecode(newCtx, classData);
267+
268+
// Create a Runnable to execute the subroutine creation
269+
Runnable subroutineCreationTask = () -> {
270+
271+
// The following commented-out code is a work in progress for handling concurrency
256272
// Class<?> generatedClass = null;
257273
// try {
258274
// semaphore.acquire();
@@ -267,39 +283,41 @@ public static Node parseSubroutineDefinition(Parser parser, boolean wantName, St
267283
// semaphore.release();
268284
// }
269285

270-
// System.out.println("Class " + generatedClass);
271-
// EmitterMethodCreator.debugInspectClass(generatedClass);
272-
try {
273-
Class<?>[] parameterTypes = classList.toArray(new Class<?>[0]);
274-
Constructor<?> constructor = generatedClass.getConstructor(parameterTypes);
275-
// System.out.println("Constructor: " + constructor);
276-
277-
Object[] parameters = paramList.toArray();
278-
code.codeObject = constructor.newInstance(parameters);
279-
// System.out.println("Instance: " + instance);
280-
281-
code.methodObject = generatedClass.getMethod("apply", RuntimeArray.class, int.class);
282-
// System.out.println("Method: " + method);
283-
284-
} catch (Exception e) {
285-
throw new PerlCompilerException("Subroutine error: " + e.getMessage());
286-
}
287-
// System.out.println("Subroutine " + fullName + " created");
288-
289-
// Clear the compilerThread once done
290-
code.compilerThread = null;
291-
};
286+
// System.out.println("Class " + generatedClass);
287+
// EmitterMethodCreator.debugInspectClass(generatedClass);
288+
try {
289+
// Prepare constructor with the captured variable types
290+
Class<?>[] parameterTypes = classList.toArray(new Class<?>[0]);
291+
Constructor<?> constructor = generatedClass.getConstructor(parameterTypes);
292+
// System.out.println("Constructor: " + constructor);
293+
294+
// Instantiate the subroutine with the captured variables
295+
Object[] parameters = paramList.toArray();
296+
code.codeObject = constructor.newInstance(parameters);
297+
// System.out.println("Instance: " + instance);
298+
299+
// Retrieve the 'apply' method from the generated class
300+
code.methodObject = generatedClass.getMethod("apply", RuntimeArray.class, int.class);
301+
// System.out.println("Method: " + method);
302+
303+
} catch (Exception e) {
304+
// Handle any exceptions during subroutine creation
305+
throw new PerlCompilerException("Subroutine error: " + e.getMessage());
306+
}
307+
// System.out.println("Subroutine " + fullName + " created");
292308

293-
// Start the subroutine creation in a new thread
294-
Thread subroutineThread = new Thread(subroutineCreationTask);
295-
subroutineThread.start();
309+
// Clear the compilerThread once done
310+
code.compilerThread = null;
311+
};
296312

297-
// Set the compilerThread in the RuntimeCode instance
298-
code.compilerThread = subroutineThread;
313+
// Use an ExecutorService with virtual threads
314+
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
315+
Future<?> future = executor.submit(subroutineCreationTask);
316+
executor.shutdown();
317+
code.compilerThread = future;
299318

300-
// return an empty AST list
301-
return new ListNode(parser.tokenIndex);
302-
}
319+
// return an empty AST list
320+
return new ListNode(parser.tokenIndex);
303321
}
304322

305323
private static SubroutineNode handleAnonSub(Parser parser, String subName, String prototype, List<String> attributes, BlockNode block, int currentIndex) {

src/main/java/org/perlonjava/runtime/RuntimeCode.java

+9-3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import java.lang.reflect.InvocationTargetException;
1515
import java.lang.reflect.Method;
1616
import java.util.*;
17+
import java.util.concurrent.ExecutionException;
18+
import java.util.concurrent.Future;
1719

1820
import static org.perlonjava.runtime.GlobalVariable.getGlobalVariable;
1921
import static org.perlonjava.runtime.RuntimeScalarCache.scalarUndef;
@@ -57,7 +59,7 @@ protected boolean removeEldestEntry(Map.Entry<String, Class<?>> eldest) {
5759
public RuntimeList constantValue;
5860

5961
// Field to hold the thread compiling this code
60-
public Thread compilerThread;
62+
public Future<?> compilerThread;
6163

6264
/**
6365
* Constructs a RuntimeCode instance with the specified prototype and attributes.
@@ -450,9 +452,11 @@ public RuntimeList apply(RuntimeArray a, int callContext) {
450452
// Wait for the compilerThread to finish if it exists
451453
if (this.compilerThread != null) {
452454
try {
453-
this.compilerThread.join(); // Wait for the thread to finish
455+
this.compilerThread.get(); // Wait for the task to finish
454456
} catch (InterruptedException e) {
455457
throw new PerlCompilerException("Thread interrupted while waiting for subroutine to compile: " + e.getMessage());
458+
} catch (ExecutionException e) {
459+
throw new PerlCompilerException("Exception occurred during subroutine compilation: " + e.getCause().getMessage());
456460
}
457461
}
458462

@@ -479,9 +483,11 @@ public RuntimeList apply(String subroutineName, RuntimeArray a, int callContext)
479483
// Wait for the compilerThread to finish if it exists
480484
if (this.compilerThread != null) {
481485
try {
482-
this.compilerThread.join(); // Wait for the thread to finish
486+
this.compilerThread.get(); // Wait for the task to finish
483487
} catch (InterruptedException e) {
484488
throw new PerlCompilerException("Thread interrupted while waiting for subroutine to compile: " + e.getMessage());
489+
} catch (ExecutionException e) {
490+
throw new PerlCompilerException("Exception occurred during subroutine compilation: " + e.getCause().getMessage());
485491
}
486492
}
487493

0 commit comments

Comments
 (0)