LLVM не оптимизирует состав функций в ИК

#c #llvm

Вопрос:

У меня есть JIT-компилятор на основе LLVM, и у меня возникли проблемы с оптимизацией функций с помощью шаблона func1(func2(x)) . Проблему можно продемонстрировать на следующем примере:

 #include <cmath>


extern "C" double transform_4326_900913_x(const double x) {
  return x * 111319.490778;
}

extern "C" double transform_4326_900913_y(const double y) {
  return 6378136.99911 * tan(log(.00872664626 * y   .785398163397));
}

extern "C" void transform(double* out, const double x_in, const double y_in) {
    out[0] = transform_4326_900913_x(x_in);
    out[1] = transform_4326_900913_y(y_in);
}

double program(const double x_in, const double y_in) {
    double arr[2];
    transform(arr, x_in, y_in);
    return arr[0];
}
 

Если я скомпилирую этот код в LLVM с помощью clang и не буду использовать быструю математику, я получу:

   %3 = fmul double %0, 0x40FB2D77DA3A083A, !dbg !416
  call void @llvm.dbg.value(metadata double %3, metadata !407, metadata !DIExpression(DW_OP_LLVM_fragment, 0, 64)), !dbg !411
  call void @llvm.dbg.value(metadata double %1, metadata !367, metadata !DIExpression()) #4, !dbg !417
  %4 = fmul double %1, 0x3F81DF46A252DD11, !dbg !419
  %5 = fadd double %4, 0x3FE921FB54441D52, !dbg !420
  %6 = tail call double @log(double %5) #4, !dbg !421
  %7 = tail call double @tan(double %6) #4, !dbg !422
}
 

Это ожидается-llvm не знает, вызывает ли stdlib log и tan может ли быть удален без влияния на программу. Если я переключусь на -ffast-math , то все будет хорошо оптимизировано:

 define dso_local double @_Z7programdd(double %0, double %1) local_unnamed_addr #1 !dbg !398 {
  %3 = fmul fast double %0, 0x40FB2D77DA3A083A, !dbg !413
  ret double %3, !dbg !414
}
 

У меня есть аналогичная проблема в моей системе, следующая той же схеме, что и выше. Рассмотрим следующие ИК:

 ST_X_nullcheck_false:                             ; preds = %groupby_nullcheck_true
   = call i8* @array_buff(i8* %col_buf0, i64 %pos) #14
  %ST_Transform_Array2 = alloca [2 x double], align 8
  %ST_Transform_Array2.sub = getelementptr inbounds [2 x double], [2 x double]* %ST_Transform_Array2, i64 0, i64 0
   = bitcast i8*  to i32*
  %compressed_x_coord = load i32, i32* , align 4
   = call double @decompress_x_coord_geoint(i32 %compressed_x_coord) #14
  store double , double* %ST_Transform_Array2.sub, align 8
   = getelementptr i8, i8* , i64 4
   = bitcast i8*  to i32*
  %compressed_y_coord = load i32, i32* , align 4
   = call double @decompress_y_coord_geoint(i32 %compressed_y_coord) #14
   = getelementptr inbounds [2 x double], [2 x double]* %ST_Transform_Array2, i64 0, i64 1
  store double , double* , align 8
   = fmul double , 0x40FB2D77DA3A083A
  store double , double* %ST_Transform_Array2.sub, align 8
   = fmul double , 0x3F81DF46A252DD11
   = fadd double , 0x3FE921FB54441D52
    = call double @Tan(double ) #15
  ! = call double @ln(double  ) #15
  " = fmul double !, 0x415854A63FF16B12
  store double ", double* , align 8
  %x_coord2 = load double, double* %ST_Transform_Array2.sub, align 8
  br label %filter_false
}

filter_false:                                     ; preds = %groupby_nullcheck_true, %ST_X_nullcheck_false
  %ST_X_nullcheck_value = phi double [ %x_coord2, %ST_X_nullcheck_false ], [ 0x10000000000000, %groupby_nullcheck_true ]
  %3 = getelementptr inbounds i64, i64* %6, i64 1
  call void @agg_id_double_shared(i64* nonnull %3, double %ST_X_nullcheck_value)
  ret i32 0
 

Здесь я вручную добавил атрибуты ( { nobuiltin nofree norecurse nosync nounwind readnone willreturn } ) в функции @Tan и @ln , и ни один из выходных данных этих функций не используется позже в функции. Если я удаляю одну из функций (любую из них), я получаю желаемую оптимизацию-вся ветвь «y» удаляется, распределение преобразуется в скалярное значение, и скаляр считывается с помощью phi filter_false . Но что-то в составе функций, похоже, нарушает способность объединения инструкций отбирать ИК. Я играл с несколькими комбинациями атрибутов и/или проходов, но начинаю подозревать, что упустил что-то более простое.

Ответ №1:

Я смог решить эту проблему, используя только существующие проходы LLVM.

Ключ состоял в том, чтобы добавить пропуск NewGVN LLVM до объединения инструкций. NewGVN снял нагрузку здесь:

   %x_coord2 = load double, double* %ST_Transform_Array2.sub, align 8
 

И просто использовал вывод инструкции умножения в соответствующем Phi. Это позволило объединить инструкции / исключить мертвый код, чтобы объявить сначала инструкции хранилища, а затем вызовы функций мертвыми. Минимальный набор проходов для работы этой оптимизации (после прохождения аннотации пользовательской функции) оказался:

 pass_manager.add(llvm::createSROAPass());
  // mem ssa drops unused load and store instructions, e.g. passing variables directly
  // where possible
  pass_manager.add(
      llvm::createEarlyCSEPass(/*enable_mem_ssa=*/true));  // Catch trivial redundancies

  pass_manager.add(llvm::createJumpThreadingPass());  // Thread jumps.
  pass_manager.add(llvm::createCFGSimplificationPass());

  pass_manager.add(llvm::createNewGVNPass());

  pass_manager.add(llvm::createInstructionCombiningPass());
 

В приведенной выше последовательности.