#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());
В приведенной выше последовательности.