Using asm to reduce overhead with registerMethods as well as improve performance

A problem when using registerMethod often is that it is rather slow. The main reason is the heavy use of reflection. Using the asm library one can however create a much faster implementation of the same functionality that only use reflection on each class once and uses the asm library.

Example.pde

void setup() {
  CLS instance[]=new CLS[100000];
  for (int i=0; i<instance.length; i++) {
    instance[i]=new CLS();
  registerMethod("pre", instance[i]);
  }
  println(millis());
  noLoop();
}
void draw() {
  println(millis());
}
public class CLS {
  void pre() {
  }
}

Handler.pde

import java.util.*;
import processing.event.KeyEvent;
import processing.event.MouseEvent;
import org.objectweb.asm.*;
import static org.objectweb.asm.Opcodes.*;

//Overrides the handleMethod,registerMethod and unregisterMethod   Methods.
//Here the events like draw,pre and so on are processed. The String notes wich method is to be executed.
@Override
  public void handleMethods(String n, Object... ob) {
  handleMethods(this, n, ob);
}
//Registers the method
@Override
  public void registerMethod(String name, Object o) {
  registerMethod(this, name, o);
}
//Registers the method. A feature implemented in this minilibrary is that to register 
//e.g. the draw-method the method itself doesn't need to be called draw
//This means one can attach other methods to be run after draw even if they don't carry this name.
//However this remapping may only be set up once per class.
//The are only there to give the option to either give the names as arguments or as an array the contents must look like this
//names[0] -> method to be run after  draw registerMethod("draw",...)
//names[1] -> method to be run before draw registerMethod("pre" ,...)
//names[2] -> method to be run post   draw registerMethod("post",...)
//names[3] -> method to be run post when exit() is called or the window closes registerMethod("dispose",...)
//names[4] -> method to be run post when a keyEvent is registered registerMethod("keyEvent",...)
//names[5] -> method to be run post when a MouseEvent is registered registerMethod("mouseEvent",...)
public void registerMethod(String name, Object o, String... names) {
  registerMethod(this, name, o, names);
}
//Unregisters a method
@Override
  public void unregisterMethod(String name, Object o) {
  unregisterMethod(this, name, o);
}


//ClassLoader used to load the wrapperclasses
final public class DynCL extends ClassLoader {
  public Class<?> defineClass(String name, byte[]b) {
    Class<?> ret= defineClass(name.replace("/", "."), b, 0, b.length);
    return ret;
  }
}
public DynCL globCL=new DynCL();

//All registerable methods are here. This doubles as the standard-entry for the names vargs in registerMethod.
public static String regmethods[]={"draw", "pre", "post", "dispose", "keyEvent", "mouseEvent"};
//Creates a Wrapper. This is a instance of a interface wich does have the draw,pre,post,... methods and can execute them without reflection.
public <T> RegisterableWrapper<T> wrap(Class<T> cls, String... names) {
  //Edits the classname
  String clName=cls.getName().replace(".", "/");
  //Creates a classname for the Wrapperclass
  String nclnm ="wrappers/Wrapper"+cls.getSimpleName();
  ClassWriter cw=new ClassWriter(ClassWriter.COMPUTE_FRAMES);
  //Creates the class. It most notably implements the RegisterableWrapper interface
  cw.visit(V1_7, ACC_PUBLIC, nclnm, null, "java/lang/Object", new String[]{"RegisterableWrapper"});
  
  //Writes empty constructor
  MethodVisitor con=cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
  con.visitCode();
  con.visitVarInsn(ALOAD, 0);
  con.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
  con.visitInsn(Opcodes.RETURN);
  con.visitMaxs(1, 1);
  for (int i=0; i<regmethods.length; i++) {
    boolean present=methodPresent(cls, names[i]);
    //Implements the abstract Method
    /*void methodName(Object o){
     *   ((Class) o).method();
     * }
     * */
    MethodVisitor mvclPrim=cw.visitMethod(ACC_PUBLIC, regmethods[i], "(Ljava/lang/Object;)V", null, null);
    mvclPrim.visitCode();
    if (present) {
      mvclPrim.visitVarInsn(ALOAD, 1);
      mvclPrim.visitTypeInsn(CHECKCAST, clName);
      mvclPrim.visitMethodInsn(INVOKEVIRTUAL, clName, names[i], "()V", false);
    }
    mvclPrim.visitInsn(Opcodes.RETURN);
    mvclPrim.visitMaxs(2, 2);
  }
  /*Implements the KeyEvent and mouseEvent Method the same way.*/
  do {
    boolean present=methodPresent(cls, names[4]);
    MethodVisitor mvclPrim=cw.visitMethod(ACC_PUBLIC, "keyEvent", "(Ljava/lang/Object;Lprocessing/event/KeyEvent;)V", null, null);
    mvclPrim.visitCode();
    if (present) {
      mvclPrim.visitVarInsn(ALOAD, 1);
      mvclPrim.visitTypeInsn(CHECKCAST, clName);
      mvclPrim.visitVarInsn(ALOAD, 2);
      mvclPrim.visitMethodInsn(INVOKEVIRTUAL, clName, names[4], "(Lprocessing/event/KeyEvent;)V", false);
    }
    mvclPrim.visitInsn(Opcodes.RETURN);
    mvclPrim.visitMaxs(3, 3);
  } while (false);

  do {
    boolean present=methodPresent(cls, names[5]);
    MethodVisitor mvclPrim=cw.visitMethod(ACC_PUBLIC, "mouseEvent", "(Ljava/lang/Object;Lprocessing/event/MouseEvent;)V", null, null);
    mvclPrim.visitCode();
    if (present) {
      mvclPrim.visitVarInsn(ALOAD, 1);
      mvclPrim.visitTypeInsn(CHECKCAST, clName);
      mvclPrim.visitVarInsn(ALOAD, 2);
      mvclPrim.visitMethodInsn(INVOKEVIRTUAL, clName, names[4], "(Lprocessing/event/MouseEvent;)V", false);
    }
    mvclPrim.visitInsn(Opcodes.RETURN);
    mvclPrim.visitMaxs(3, 3);
  } while (false);
  cw.visitEnd();
  try {
    //Loads the class and returns a instance
    return ((Class<RegisterableWrapper<T>>)globCL.defineClass(nclnm, cw.toByteArray()))
      .getConstructor(new Class[]{}).newInstance();
  }
  catch(Exception e) {
    throw new RuntimeException(e);
  }
}
//Creates a wrapper without needing to input the names
public <T> RegisterableWrapper<T> wrap(Class<T> cls) {
  return wrap(cls, regmethods);
}
//Checks if of a certain name exists in a class
public static boolean methodPresent(Class<?> cls, String name) {
  if(name.length()==0) return false;
  try {
    while (true) {
      java.lang.reflect.Method[] mts=cls.getDeclaredMethods();
      for (java.lang.reflect.Method m:mts) {
        if (m.getName().equals(name)) return true;
      }
      if (cls==Object.class) return false;
      cls=cls.getSuperclass();
    }
  }
  catch(Exception e) {
    throw new RuntimeException(e);
  }
}


//Tracks wrappers used so only one instance needs to be created
Map<Class<?>, RegisterableWrapper<Object>> wrapperMap=new HashMap<Class<?>, RegisterableWrapper<Object>>();
//Tracks registered methods. This contains e.g. all registered draw methods.
public class RegisterMethodHandler {
  //Tracks objects
  List<Object> objs=new ArrayList<Object>();
  //Tracks wrappers
  List<RegisterableWrapper<Object>> wrps=new ArrayList<RegisterableWrapper<Object>>();
  //Adds a object to the list
  public void register(Object obj, String... names) {
    Class<?> cls=obj.getClass();
    //If there is no wrapper present one will be created
    if (!wrapperMap.containsKey(cls)) declareMap(cls, names);
    objs.add(obj);
    wrps.add(wrapperMap.get(cls));
  }
  public void register(Object obj) {
    register(obj, regmethods);
  }
  //Removes an object from the handler
  public void unregister(Object obj) {
    for (int i=0; i<objs.size(); i++) {
      if (objs.get(i)==obj) {
        objs.remove(i);
        wrps.remove(i);
        return;
      }
    }
  }
  //Executes the method by checking the name and executing the according method
  public void executeMethod(String name, Object... args) {
    switch(name) {
      case "draw":
        for (int i=0; i<objs.size(); i++) wrps.get(i).draw(objs.get(i));
      break;
      case "pre":
        for (int i=0; i<objs.size(); i++) wrps.get(i).pre(objs.get(i));
      break;
      case "post":
        for (int i=0; i<objs.size(); i++) wrps.get(i).post(objs.get(i));
      break;
      case "dispose":
        for (int i=0; i<objs.size(); i++) wrps.get(i).dispose(objs.get(i));
      break;
      case "keyEvent":
        for (int i=0; i<objs.size(); i++) wrps.get(i).keyEvent(objs.get(i), (processing.event.KeyEvent)args[0]);
      break;
      case "mouseEvent":
        for (int i=0; i<objs.size(); i++) wrps.get(i).mouseEvent(objs.get(i), (processing.event.MouseEvent)args[0]);
      break;
    default:
      throw new RuntimeException("Unrecognized Method: "+name);
    }
  }
}


//Contains the data about all PApplets, and wich methods are registere (The PApplets are tracked because this code is part of a small library)
public static Map<PApplet, Map<String, RegisterMethodHandler>> collected=new HashMap<PApplet, Map<String, RegisterMethodHandler>>();

//Handles the Registering by putting the method and object into the correct data-structure
public void registerMethod(PApplet pa, String name, Object o, String... names) {
  if (!collected.containsKey(pa)) collected.put(pa, new HashMap<String, RegisterMethodHandler>());
  if (!collected.get(pa).containsKey(name)) collected.get(pa).put(name, new RegisterMethodHandler());
  RegisterMethodHandler rmh=collected.get(pa).get(name);
  rmh.register(o, names);
}
public void registerMethod(PApplet pa, String name, Object o) {
  registerMethod(pa, name, o, regmethods);
}
public void unregisterMethod(PApplet pa, String name, Object o) {
  collected.get(pa).get(name).unregister(o);
}
//executes handled Methods
public void handleMethods(PApplet pa, String name, Object... args) {
  //Checks if the method is even registered
  if (!collected.containsKey(pa)) return;
  if (!collected.get(pa).containsKey(name)) return;
  collected.get(pa).get(name).executeMethod(name, args);
}
//Creates a wrapper and saves it
void declareMap(Class<?> cls, String... names) {
  wrapperMap.put(cls, (RegisterableWrapper<Object>)wrap(cls, names));
}

RegisterableWrapper.java

import processing.event.*;
public interface RegisterableWrapper<T> {
  public void draw(T a);
  public void pre(T a);
  public void post(T a);
  public void dispose(T a);
  public void keyEvent(T a, KeyEvent ke);
  public void mouseEvent(T a, MouseEvent me);
}

This has a much less overhead (300ms instead of 25s) in the example and less time executing (20ms between setup and draw instead of 80-120ms)

4 Likes

I modified the code in Example.pde to this

int time;

void setup() {
  time = millis();
  CLS instance[]=new CLS[100000];
  for (int i=0; i<instance.length; i++) {
    instance[i]=new CLS();
    registerMethod("pre", instance[i]);
  }
  println(millis() - time);
  time = millis();
  noLoop();
}

void draw() {
  println(millis() - time);
}

public class CLS {
  void pre() {
  }
}

because it more accurately calculates the time to “register the methods”, by removing the Processing launch time.

The results on my computer were

  • Using ASM : 28 & 10ms
  • Without ASM : 16462 & 34ms

which is a very significant improvement but it is unlikely that you will have a 100 thousand methods to register :wink:

I used ASM in my library Jasmine to provide a fast way of executing numeric expressions and algorithms stored in strings. I found understanding and using ASM one of the most challenging things I have done so I congratulate you on successfully using ASM to effectively alter the way Processing works :+1:

3 Likes

Hi @quark,

Nice idea! Used it also in the past to write a java agent for profiling existing java modules on the fly…

For others as info and starting point…
https://asm.ow2.io/

Simple examples …

Cheers
— mnse

2 Likes

An additional useful source if you want to get into bytecode maniputation yourself is this talk:
Java Bytecode Crash Course (youtube.com)
This helped me understand Java bytecode and helped me when I was rather new to this.

2 Likes

This post definitely has vibes of cool … but why??? Reflection lookup has some overhead, the calling less so (and possibly already uses bytecode generation). Do you really have a bottleneck, or is this just an experiment?

A good quote from Brian Goetz, “The scary thing about microbenchmarks is that they always produce a number, even if that number is meaningless. They measure something; we’re just not sure what.” In particular, measuring a single run through draw() is pointless. Secondly, the code seems to handle methods up to the reflection call itself in quite a different way to the Processing source. Either might completely skew the results.

I’d recommend looking at ByteBuddy if you want to use ASM, or even experimenting with the new class file API in the JDK which will allow for doing this without libraries.

2 Likes

This code originally emerged from code I was writing to allow bundling functions in a way that is fast to execute.
Since I actually really like registerMethod for it’s potential to reduce boiler-plate code it troubled me that it’s implementation was quite slow and in my oppinion had wasted potential.
So I figured that I adapt the code I used for bundling methods to make a little library to improve that situation.
The version I posted here was a simplified version of this code however my original code had a other things it could do.

It is an interisting question how much time was truly saved by using asm in contrast to my implementation of registerMethod being completly different one that is certainly worth investigating.

ByteBuddy might indeed be worth looking into. Importing java.lang.classfile in processing shows that it sadly isn’t available in Processing 4 wich uses Java 17 in contrast to Java 22 needed for the classfile API it however is still a great tipp from you!

1 Like