Part 60 - obtaining compilation tasks

Posted by KrisNz on Wed, 26 Jan 2022 23:29:45 +0100

OSR or method compilation was described earlier. If you have decided to compile, you will call the SimpleThresholdPolicy::compile() function to compile. The relevant functions called are as follows:

 

 

 

1. Submit compilation task

Simplethresholdpolicy:: Submit is called in the SimpleThresholdPolicy::compile() function_ The compile() function submits the compilation task. The implementation of this function is as follows:

void SimpleThresholdPolicy::submit_compile(
 methodHandle mh,
 int bci,
 CompLevel level,
 JavaThread* thread
) {
  int hot_count = (bci == InvocationEntryBci) ? mh->invocation_count() : mh->backedge_count();
  CompileBroker::compile_method(mh, bci, level, mh, hot_count, "tiered", thread);
}

The passed in parameter level is usually the target compilation level, which is calculated by calling the AdvancedThresholdPolicy::common() function introduced in the previous article.

If it is OSR compilation, refer to the back edge count; if it is method compilation, refer to the method call count. Finally, compilebroker:: compile will be called_ Method() function.

nmethod* CompileBroker::compile_method(
 methodHandle   method,
 int            osr_bci,
 int            comp_level,
 methodHandle   hot_method,
 int            hot_count,
 const char*   comment,
 Thread*        THREAD
) {

  AbstractCompiler *comp = CompileBroker::compiler(comp_level);
  // ...

  if (osr_bci == InvocationEntryBci) { // Method compilation
    nmethod* method_code = method->code();
    if (method_code != NULL) {
      if (compilation_is_complete(method, osr_bci, comp_level)) {
        return method_code;
      }
    }
    if (method->is_not_compilable(comp_level)) {
      return NULL;
    }
  } else { // Replace on stack
    nmethod* nm = method->lookup_osr_nmethod_for(osr_bci, comp_level, false);
    if (nm != NULL)
    	return nm;
    if (method->is_not_osr_compilable(comp_level))
    	return NULL;
  }

  // If it is a local method and not an intrinsic related method, call NativeLookup to load the corresponding local code
  if (method->is_native() && !method->is_method_handle_intrinsic()) {
    bool in_base_library;
    address adr = NativeLookup::lookup(method, in_base_library, THREAD);
  }

  if (method->is_native()) {
     // Prefererpreternativestubs indicates whether local methods are always called using the stub of the interpreter. The default value is false
    if (!PreferInterpreterNativeStubs || method->is_method_handle_intrinsic()) {
      int compile_id;
      {
        MutexLocker locker(MethodCompileQueue_lock, THREAD);
        compile_id = assign_compile_id(method, standard_entry_bci);
      }
      // Notice the create called here_ native_ The wrapper () function generates an entry for compilation execution for the local method, which is
      // I explained it in detail before
      char* xx =  method->name()->as_C_string();
      (void) AdapterHandlerLibrary::create_native_wrapper(method, compile_id);
    } else {
      return NULL;
    }
  } else {
    // If the CodeCache in which the code is stored is full, NULL will be returned directly, so that the continuation of the method will not be blocked
    // implement
    if (!should_compile_new_jobs()) {
       CompilationPolicy::policy()->delay_compilation(method());
       return NULL;
    }
    compile_method_base(method, osr_bci, comp_level, hot_method, hot_count, comment, THREAD);
  }

  if(osr_bci  == InvocationEntryBci){ // Method compilation
   return  method->code() ;
  }else{ // OSR compilation
   return  method->lookup_osr_nmethod_for(osr_bci, comp_level, false);
  }
}  

Call to determine whether C1 or C2 Compiler is used for method compilation. C1 and C2 have AbstractCompiler interfaces, which are C1 Compiler and C2 Compiler respectively. The function is implemented as follows:

static AbstractCompiler* compiler(int comp_level) {
    if (is_c2_compile(comp_level)){
    	return _compilers[1]; // C2
    }
    if (is_c1_compile(comp_level)){
    	return _compilers[0]; // C1
    }
    return NULL;
}

Among them_ The initialization of compilers was introduced earlier. The stored compilers are C1 and C2 respectively.   

If it is a local method, you need to call adapterhandlerlibrary:: create_ native_ The wrapper () function generates an entry routine for compilation execution, which was described in detail earlier.

If it is a non local method, call compile_ method_ The base() function submits the compilation task. The implementation of this function is as follows:

void CompileBroker::compile_method_base(
 methodHandle  method,
 int           osr_bci,
 int           comp_level,
 methodHandle  hot_method,
 int           hot_count,
 const char*   comment,
 Thread*       thread
) { 

  // Judge whether it is necessary to compile this compilation. When a suitable compilation result of this compilation can be obtained
  // Just return directly
  if (compilation_is_complete(method, osr_bci, comp_level)) {
    return;
  }

  // If the compilation task already exists in the compilation queue, it will be returned directly
  if (compilation_is_in_queue(method, osr_bci)) {
    return;
  }

  // ...

  
  CompileTask*   task     = NULL;
  bool           blocking = false;
  // Check the compilation queue used by the compiler corresponding to this compilation task. C1 uses compilebroker:_ c2_ method_ queue,
  // C2 uses compilebroker:_ c1_ method_ queue
  CompileQueue* queue  = compile_queue(comp_level);

  
  { // Anonymous block start    
    MutexLocker locker(queue->lock(), thread);

    // When the compilation task is already in the compilation queue, it returns directly
    if (compilation_is_in_queue(method, osr_bci)) {
      return;
    }

    // Since the compilation may have been completed before, return directly
    if (compilation_is_complete(method, osr_bci, comp_level)) {
      return;
    }

    uint compile_id = assign_compile_id(method, osr_bci);
    if (compile_id == 0) {
      return;
    }

    // Judge whether the thread needs to wait until the compilation task is completed before continuing. It will judge the value of the BackgroundCompilation option, which is true by default,
    // Therefore, it is usually compiled in the background without waiting
    // When the method has a breakpoint, it is not compiled. When the parameter - XX:-BackgroundCompilation is set to not background compilation,
    // It does not mean that the task is compiled in the user thread, but submits the task CompileTask to the CompileQueue. The only difference is to block the current thread waiting
    // CompileThread until the Task is compiled successfully
    blocking = is_compile_blocking(method, osr_bci);
    // Create compilation task
    task = create_compile_task(
		   queue,
		   compile_id,
		   method,
		   osr_bci,
		   comp_level,
		   hot_method,
		   hot_count,
		   comment,
		   blocking
	 );
  } // Anonymous block end

  // If blocking is true, it means that you need to wait for the compilation task to complete before continuing to execute the method, such as OSR compilation
  if (blocking) {
    wait_for_completion(task);
  }
}

Called create_ compile_ The task() function creates a compilation task. The implementation of the function is as follows:

CompileTask* CompileBroker::create_compile_task(
  CompileQueue* queue,
  int           compile_id,
  methodHandle  method,
  int           osr_bci,
  int           comp_level,
  methodHandle  hot_method,
  int           hot_count,
  const char*   comment,
  bool          blocking
) {
  CompileTask* new_task = allocate_task();
  new_task->initialize(compile_id, method, osr_bci, comp_level,hot_method, hot_count, comment,blocking);
  queue->add(new_task);
  return new_task;
}

Create a CompileTask task and add it to the task queue of C1 or C2. This function returns directly. This task is fetched from the queue by the C1 compiler thread or the C2 compiler thread, and then compiled. Whether the function submitting the task needs to wait for the compilation task to complete depends on the value of the BackgroundCompilation option. The default value is true, indicating asynchronous compilation, so it will not wait for the compilation to complete.

2. Take the task from the CompileQueue queue to compile

The call stack is as follows:

Threads::create_vm()  thread.cpp	
CompileBroker::compilation_init()  compileBroker.cpp	

Compilation is called when a virtual machine instance is created_ Init() function initializes CompileBroker, compilation_ The init() function is responsible for initializing compilation related components, including compiler implementation, compilation threads, counters, etc. CompileBroker:: make is also called_ compiler_ Thread() function, in which the corresponding compilation thread is created and started, and the new thread will call compiler_ thread_ The loop () function takes the task from the CompileQueue to compile. The call stack of the new compilation thread started is as follows:

clone()                                clone.S
start_thread()                         pthread_create.c
java_start()                           os_linux.cpp
JavaThread::run()                      thread.cpp
JavaThread::thread_main_inner()        thread.cpp
compiler_thread_entry()                thread.cpp
CompileBroker::compiler_thread_loop()  compileBroker.cpp

The compilation thread will continuously obtain the compilation task from the compilation task queue, and then compile the compiler_ thread_ The loop() function is implemented as follows:

void CompileBroker::compiler_thread_loop() {
  CompilerThread* thread = CompilerThread::current();
  CompileQueue* queue = thread->queue();

  // Only when the compiler fails to initialize due to insufficient CodeCache memory during initialization, compilation will be disabled
  // If compilation is not disabled, the initialization is normal, and is_ compilation_ disabled_ The forever() function always returns false
  while (!is_compilation_disabled_forever()) {
    HandleMark hm(thread);    

   // Get a new task from the compilation queue. If there is no new compilation task, the get() method waits for 5s
    CompileTask* task = queue->get();
    if (task == NULL) {
      continue;
    }

    CompileTaskWrapper ctw(task);
    nmethodLocker result_handle;  
    task->set_code_handle(&result_handle);
    methodHandle method(thread, task->method());

    // Compilation occurs only if there are no breakpoints
    if (method()->number_of_breakpoints() == 0) {
      // Compile the method.
      if ((UseCompiler || AlwaysCompileLoopMethods) && 
                  CompileBroker::should_compile_new_jobs()) {
        ...
        invoke_compiler_on_method(task); // Compile the compilation task
      } else {
        // If compilation has been prohibited, the compilation task in the queue is removed
        method->clear_queued_for_compilation();
      }
    }
  }

  shutdown_compiler_runtime(thread->compiler(), thread);
}

After the compilation thread is started, it automatically executes the above function, circularly takes out the compilation task from the compilation task queue to perform compilation, and makes some judgments after obtaining a new compilation task. For example, if the method has a breakpoint, it will not be compiled. square

Let's look at invoke_ compiler_ on_ The method() function is implemented as follows:

void CompileBroker::invoke_compiler_on_method(CompileTask* task) {
  // ...
  int osr_bci = task->osr_bci(); 
  bool is_osr = (osr_bci != standard_entry_bci);
  int task_level = task->comp_level();

  { 
      ciEnv ci_env(task, system_dictionary_modification_counter);

      // After the Method * is passed in, the ciMethod * is returned. The ciMethod will be operated during compilation*
      ciMethod* target = ci_env.get_method_from_handle(target_handle);

      AbstractCompiler *comp = compiler(task_level);
      comp->compile_method(&ci_env, target, osr_bci);
   }
  // ...
}

Note that whether it is OSR or method compilation, compiler:: compile will eventually be called_ Method() to compile.

Here we also need to pay attention to a series of classes starting with ci, such as ciEnv and ciMethod. ci is the abbreviation of compiler interface. Since there are various compilers in HotSpot VM, compiler related interfaces are specially defined. When we need to compile methods, the relevant data will be encapsulated as some class instances starting with ci. For example, call get_ Method_ from_ The handle () Method encapsulates Method * as ciMethod.

The implementation of the compiler() function is as follows:

static AbstractCompiler* compiler(int comp_level) {
    if (is_c2_compile(comp_level))
    	return _compilers[1]; // C2 compiler
    if (is_c1_compile(comp_level))
    	return _compilers[0]; // C1 compiler
    return NULL;
}

For C1, the Compiler of the Compiler will be called_ Method() function; For C2, the compile of C2Compiler will be called_ Method() function. Here we only look at the compilation of C1 and the compilation:: compile called_ The important implementation of the method() function is as follows:

// Compile method or OSR
int frame_size = compile_java_method();
// ...

if (InstallMethods) { // The default value of the InstallMethods option is true
  // Installation method or OSR
  install_code(frame_size);
}

Call compile_java_method() function compilation. This method is the entry method of C1 compiler. Later, when introducing C1 compiler, we will start with the implementation of this function. After the compilation is completed, call install_. The code() function "installs" the code. The next article will detail the process of installing and uninstalling code after compiling OSR and methods.

The official account is analyzed in depth. Java virtual machine HotSpot has updated the VM source code to analyze the related articles to 60+. Welcome to the attention. If there are any problems, add WeChat mazhimazh, pull you into the virtual cluster communication.