2016-09-21 4 views
11

Ich habe viele kleine Funktionen, die ich Inline möchte, zum Beispiel Fahnen für eine Bedingung zu testen:Julia - wie funktioniert @inline? Wann man Funktion gegen Makro benutzt?

const COND = UInt(1<<BITS_FOR_COND) 
function is_cond(flags::UInt) 
    return flags & COND != 0 
end 

ich auch einen Makro machen könnte:

macro IS_COND(flags::UInt) 
    return :(flags & COND != 0) 
end 

Meine Motivation sind viele ähnlichen Makrofunktionen mit dem ich in der C-Code arbeite:

#define IS_COND(flags) ((flags) & COND) 

I timed wiederholt die Funktion, Makro, Funktion mit @inline definiert, und die Expression von selbst, sondern Keine sind in vielen Läufen konsistent schneller als die anderen. Der generierte Code für den Funktionsaufruf in 1) und 3) ist viel länger als für den Ausdruck in 4), aber ich weiß nicht, wie man 2) vergleicht, da @code_llvm usw. nicht auf anderen Makros funktionieren.

1) for j=1:10 @time for i::UInt=1:10000 is_cond(i); end end 
2) for j=1:10 @time for i::UInt=1:10000 @IS_COND(i); end end 
3) for j=1:10 @time for i::UInt=1:10000 is_cond_inlined(i); end end 
4) for j=1:10 @time for i::UInt=1:10000 i & COND != 0; end end 

Fragen: Was ist der Zweck der @inline? Ich sehe aus der spärlichen Dokumentation, dass es das Symbol :inline an den Ausdruck :meta anhängt, aber was macht das genau? Gibt es einen Grund, eine Funktion oder ein Makro für diese Art von Aufgabe zu bevorzugen?

Mein Verständnis ist, dass eine C-Makro-Funktion nur den literalen Text des Makros zur Kompilierzeit ersetzt, so dass der resultierende Code keine Sprünge und ist daher effizienter als ein normaler Funktionsaufruf. (Sicherheit ist ein anderes Problem, aber nehmen wir an, die Programmierer wissen, was sie tun.) Ein Julia-Makro hat Zwischenschritte wie das Parsen seiner Argumente, also ist es für mich nicht offensichtlich, ob 2) schneller als 1) sein sollte. Ignorieren Sie für den Moment, dass in diesem Fall der Unterschied in der Leistung vernachlässigbar ist, welche Technik den effizientesten Code ergibt?

Antwort

17

Wenn die beiden Syntaxen genau denselben generierten Code ergeben, sollten Sie einen über den anderen bevorzugen? JA. Funktionen sind Makros in solchen Situationen weit überlegen.

  • Makros sind leistungsstark, aber sie sind knifflig. Sie haben drei Fehler in Ihrer @IS_COND Definition (Sie möchten nicht eine Typ Annotation auf das Argument setzen, müssen Sie flags in den zurückgegebenen Ausdruck interpolieren, und Sie müssen esc verwenden, um die Hygiene richtig zu bekommen).
  • Die Funktionsdefinition funktioniert so, wie Sie es erwarten.
  • Vielleicht noch wichtiger ist, funktioniert die Funktion wie andere erwarten würde. Makros können alles, so dass @ Sigil ist eine gute Warnung für "etwas über normale Julia-Syntax tritt hier." Wenn es sich jedoch genau wie eine Funktion verhält, könnte es auch eins machen.
  • Funktionen sind erstklassige Objekte in Julia; Sie können sie weitergeben und sie mit Funktionen höherer Ordnung wie map verwenden.
  • Julia ist auf Inlinefunktionen aufgebaut. Seine Leistung hängt davon ab! Kleine Funktionen benötigen normalerweise nicht einmal die @inline Annotation - sie macht es nur für sich. Sie können @inline verwenden, um dem Compiler einen zusätzlichen Hinweis zu geben, dass eine größere Funktion besonders wichtig für Inline ist ... aber oft ist Julia gut darin, es selbst herauszufinden (wie hier).
  • Backtraces und Debugging funktionieren besser mit eingebetteten Funktionen als Makros.

So, jetzt, nicht zur Folge haben sie in dem gleichen generierten Code? Eines der mächtigsten Dinge an Julia ist deine Fähigkeit, sie um ihre "Zwischenarbeit" zu bitten.

Zuerst etwas ein:

julia> const COND = UInt(1<<7) 
     is_cond(flags) = return flags & COND != 0 
     macro IS_COND(flags) 
      return :($(esc(flags)) & COND != 0) # careful! 
     end 

Jetzt können wir beginnen bei der Suche, was passiert, wenn Sie entweder is_cond oder @IS_COND verwenden. Im eigentlichen Code, werden Sie diese Definitionen in anderen Funktionen verwenden möchten, so lassen Sie uns einige Testfunktionen erstellen:

julia> test_func(x) = is_cond(x) 
     test_macro(x) = @IS_COND(x) 

Jetzt können wir die Kette nach unten bewegen beginnen, um zu sehen, ob es einen Unterschied gibt. Der erste Schritt ist das "Senken" - dies wandelt die Syntax einfach in eine begrenzte Teilmenge um, um dem Compiler das Leben zu erleichtern. Sie können, dass in diesem Stadium sehen, wird das Makro erweitert, aber der Funktionsaufruf bleibt noch:

julia> @code_lowered test_func(UInt(1)) 
LambdaInfo template for test_func(x) at REPL[2]:1 
:(begin 
     nothing 
     return (Main.is_cond)(x) 
    end) 

julia> @code_lowered test_macro(UInt(1)) 
LambdaInfo template for test_macro(x) at REPL[2]:2 
:(begin 
     nothing 
     return x & Main.COND != 0 
    end) 

Der nächste Schritt ist jedoch, Inferenz und Optimierung. Es ist hier, dass Funktion Inlining wirksam wird:

julia> @code_typed test_func(UInt(1)) 
LambdaInfo for test_func(::UInt64) 
:(begin 
     return (Base.box)(Base.Bool,(Base.not_int)((Base.box)(Base.Bool,(Base.and_int)((Base.sle_int)(0,0)::Bool,((Base.box)(UInt64,(Base.and_int)(x,Main.COND)) === (Base.box)(UInt64,0))::Bool)))) 
    end::Bool) 

julia> @code_typed test_macro(UInt(1)) 
LambdaInfo for test_macro(::UInt64) 
:(begin 
     return (Base.box)(Base.Bool,(Base.not_int)((Base.box)(Base.Bool,(Base.and_int)((Base.sle_int)(0,0)::Bool,((Base.box)(UInt64,(Base.and_int)(x,Main.COND)) === (Base.box)(UInt64,0))::Bool)))) 
    end::Bool) 

Schau dir das an! Dieser Schritt in der internen Darstellung ist ein wenig unordentlicher, aber Sie können sehen, dass die Funktion inline wurde (auch ohne @inline!) Und jetzt sieht der Code genau identisch zwischen den beiden aus.

Wir gehen weiter und fragen Sie nach dem LLVM ... und in der Tat die beiden sind genau identisch:

julia> @code_llvm test_func(UInt(1))  | julia> @code_llvm test_macro(UInt(1)) 
              | 
define i8 @julia_test_func_70754(i64) #0 { | define i8 @julia_test_macro_70752(i64) #0 { 
top:          | top: 
    %1 = lshr i64 %0, 7      | %1 = lshr i64 %0, 7 
    %2 = xor i64 %1, 1      | %2 = xor i64 %1, 1 
    %3 = trunc i64 %2 to i8     | %3 = trunc i64 %2 to i8 
    %4 = and i8 %3, 1      | %4 = and i8 %3, 1 
    %5 = xor i8 %4, 1      | %5 = xor i8 %4, 1 
    ret i8 %5        | ret i8 %5 
}           | } 
+0

Fantastische Antwort, danke. Code-Introspection-Dienstprogramme arbeiteten nicht an dem Makro selbst, aber ich habe nicht erwogen, das Makro in einer Testfunktion zu umbrechen. Frage: Warum ist es schlecht, Arten von Makroargumenten zu kommentieren? –

+2

Es ist nicht unbedingt schlecht, es macht einfach nicht das, was du erwartest. Makroargumenttypen beziehen sich auf die Syntaxbaumdarstellung und nicht auf die tatsächlichen Arten von Laufzeitwerten. Zum Beispiel könnte "@foo (1)" an "Makro foo (:: Int)" senden, aber "x = 1; @foo (x) 'sendet an' foo (:: Symbol) 'und' @foo (Int (1)) 'sendet an' foo (:: Expr) '. Diese Unterscheidungen sind normalerweise nicht hilfreich, deshalb möchten Sie das fast nie tun. –