Beefy Boxes and Bandwidth Generously Provided by pair Networks
Problems? Is your data what you think it is?
 
PerlMonks  

RFC: Inline::Blocks or inline as a keyword?

by shmem (Chancellor)
on Jul 30, 2018 at 06:26 UTC ( [id://1219450]=perlmeditation: print w/replies, xml ) Need Help??

Some days ago, Ovid on the perl5porters mailing list asked under the subject "inline keyword?"

I know people have tried to inline Perl subs before but with little success. Instead of going down that road, have we ever considered an inline keyword where the developer says what can be inlined?

There have been only a few answers, most off the point (mine included in the latter).

I haven't considered inlining of subroutines as do blocks at all up to now, and whipped up some benchmark code to prove Ovids point.

use Benchmark qw(cmpthese); my $end = 500_000; cmpthese( -1, { plain => sub { my $total; for(1..$end) { $total += 1 / $_; } $total; }, sub => sub { my $total; for(1..$end) { $total += reciprocal($_); } $total; }, do => sub { my $total; for(1..$end) { $total += do { 1 / $_ }; } $total; }, do_var => sub { my $total; for(1..$end) { $total += do{ my $int = $_; 1 / $int }; } $total; }, } ); sub reciprocal { my ($int) = @_; 1 / $int; } __END__ Rate sub do_var plain do sub 6.60/s -- -49% -72% -72% do_var 12.9/s 95% -- -46% -46% plain 23.9/s 261% 85% -- 0% do 23.9/s 261% 85% 0% --

That's pretty impresssive. But the inlined subroutine consists of just one integer division.

Experimenting further, I found that adding seven reciprocals as 1 / $_; to the plain sub set it in par with the sub subroutine. This means that the overhead of calling a subroutine against plain inlined code is that of seven integer divisions. With heavily used small subs, inlining gives the most benefit in performance; the percentage drops as those subs get more complex, but inlining can be a significant performance boost.

What do you think? Should we have an inline keyword in Perl?

Or should we delegate that to a module?

Making inline into a Perl keyword proper would mean giving it an opcode, cloning of optrees and injecting them at the places where an inlined subroutine is called. Leaving it to a module would mean source filtering.

Thinking about it, inlining subroutines isn't anything special to the language. It is just duplicating code - for some reason - all over the place, something you don't want to do statically for the sake of DRY (Don't Repeate Yourself) and avoiding a maintenance nightmare. So you want to leave that to some mechanism at compile time, let the computer do it, and don't have the results in the source code.

Since implementing inline as a keyword proper is much more difficult than whipping up a source filter, I decided to do the latter, and for being lazy.

Before you go O Noes, another source filter module o_O - that's brittle, evil eval etc think about that: the whole perl source code relies on a source code filter named C pre-processor. All perl C source code is shoven through the preprocessor prior to compilation. If preprocessing fails, there's no compilation; cpp doesn't prove whether it produces valid C code, but fails within its own rules. Validating the procuded code is a task of the compiler.

The same applies for Perl source filters. The fact that the source filter is invoked in the compile phase doesn't change that, that is just how perl works - it switches between parsing, compiling and runtime in the compile phase (think BEGIN blocks and use), so there's nothing special about it.

Source filters have their merits. For instance, IO::All is a wonderful tool in my box, and I use it where appropriate.

After that long preamble providing the rationale and defense for the perpetration, here's the module.

update: edited according to tobyinks remarks below. The match variables are no longer package globals, and overridable via import parameters.

package Inline::Blocks; use strict; use warnings; require Filter::Util::Call; our $VERSION = '0.01'; our $debug = 0; # our $callmatch ||= qr{inline\s+(\w+)\s*\(([^\n]*)\)}; # find i +nline call # our $plainmatch ||= 'qr{\b$sub\s*\(([^\n]*)\)}'; # find p +lain invocation # our $bodymatch ||= 'qr{^sub $sub\s*(\{\n.+?\n\})$}ms'; # find s +ub body # our $declmatch ||= qr{^inline\s+sub\s+(\w+)\s*(?:;|\{)}ms; # find s +ub declaration my $callmatch = qr{inline\s+(\w+)\s*\(([^\n]*)\)}; # find inline + call my $plainmatch = 'qr{\b$sub\s*\(([^\n]*)\)}'; # find plain +invocation my $bodymatch = 'qr{^sub $sub\s*(\{\n.+?\n\})$}ms'; # find sub bo +dy my $declmatch = qr{^inline\s+sub\s+(\w+)\s*(?:;|\{)}ms; # find sub de +claration sub import { shift if $_[0] eq __PACKAGE__; @_ % 2 and die "odd number of arguments passed to ".__PACKAGE__. '->import, aborted'; my %args = @_; my $callmatch = delete $args{callmatch} || $callmatch; my $plainmatch = delete $args{plainmatch} || $plainmatch; my $bodymatch = delete $args{bodymatch} || $bodymatch; my $declmatch = delete $args{declmatch} || $declmatch; my $debug = delete $args{debug} || $debug; %args and die "unknown import parameters found (",join(", ",keys % +args), ") - aborted"; my $done; Filter::Util::Call::filter_add( sub { return 0 if $done; my $status; my $data; while (($status = Filter::Util::Call::filter_read()) > 0) +{ /^__(?:END|DATA)__\r?$/ and last; $data .= $_; $_ = ''; } $_ = $data; while (/$declmatch/g) { my $match = $&; my $sub = $1; s/inline\s+sub/sub/ms; my $re = eval $bodymatch; my ($text) = /$re/; $text or die "Couldn't find subroutine body for sub $s +ub\n"; print "sub body: '$text'\n" if $debug; $text =~ /\breturn\b/ and die "return statement found in sub '$sub'! Rea +d the documentation.\n"; my $plain = eval $plainmatch; while(/$plain/) { my $match = $&; my $args = $1; (my $repl = $text) =~ s/=\s*\@_/= ($args)/; s/\Q$match\E/do $repl/; } (my $repl = $match) =~ s/\w+\s+//; s/$match/$repl/; } while (/$callmatch/g) { my $match = $&; my $sub = $1; my $args = $2; print "matched subcall: '$match' sub '$sub' args '$arg +s'\n" if $debug; my $re = eval $bodymatch; my ($text) = /$re/; $text or die "Couldn't find subroutine body for sub $s +ub\n"; $text =~ /\breturn\b/ and die "return statement found in sub '$sub'! Rea +d the documentation.\n"; print "sub body: '$text'\n" if $debug; $text =~ s/=\s*\@_/= ($args)/; s/\Q$match\E/do $text/; } print "=== BEGIN ===\n$_\n=== END ===\n" if $debug; $done = 1; } ); } 1; __END__ =head1 NAME Inline::Blocks - inline subroutine bodies as do { } blocks =head1 SYNOPSIS # inline sub at marked locations use Inline::Blocks; sub sum_reciprocals_to { my ($end) = @_; my $total = 0; for my $int ( 1 .. $end ) { $total += inline reciprocal($int); } return $total; } sub reciprocal { 1 / $int; } # inline sub at every sub call use Inline::Blocks; sub sum_reciprocals_to { my ($end) = @_; my $total = 0; for my $int ( 1 .. $end ) { $total += reciprocal($int); } return $total; } inline sub reciprocal { 1 / $int; } # both deparse with -MO=Deparse as use Inline::Blocks; sub sum_reciprocals_to { my($end) = @_; my $total = 0; foreach my $int (1 .. $end) { $total += do { 1 / $int }; } return $total; } sub reciprocal { 1 / $int; } # roll your own declmatch, turn on debug use Inline::Blocks ( declmatch => qr{^metastasize\s+sub\s+(\w+)\s*(?:;|\{)}ms, debug => 1, ); metastasize sub capitalize_next; =head1 DESCRIPTION This is a module for inlining subroutines as C<do> blocks for performa +nce reasons implemented as a source filter. It is not a fully fledged macro expans +ion module. This module provides a new keyword, C<inline> by default, which is use +d to prefix either subroutine calls or subroutine declarations/definitions. If a subroutine declaration or definition is marked as C<inline>, all +instances of subroutine calls are replaced with a C<do> block containing the subrou +tine's body. If a subroutine isn't declared als inlined, only the calls to that sub + marked as C<inline> are transformed into C<do> blocks, other instances are left +as is. =head2 Conventions Currently, only plain named subroutines can be inlined (but see "Overr +iding" below). This means that subroutines which prototypes or attributes are not sui +table for inlining. Inlineable subroutines MUST NOT use C<return>, since in a C<do> block +this would cause a return from the inlinee, i.e. return from a sub which uses inl +ined code. The return value is the latest statement of the subroutine. For subs w +ith multiple return points, use a variable to assign it the value and arrange your +code so that it always reaches the last subroutine statement which contains the var +iable. A subroutine block is used textually, as is, so identifiers not privat +e to the subroutine will be those of the scope into which that block is inlined +. Subroutines which are closures are not suitable for inlining, e.g. this { my $bottom = 7; sub height { my ($rise) = @_; $bottom + $rise; } } will not use the value 7 as C<$bottom>, and compilation will fail unde +r C<use strict> if there's no C<$bottom> present in the scope of the inlined call. If parameters are passed into the subroutine, those need to be assigne +d to variables in LIST context: my ($foo, $bar) = @_; Inlining will substitute C<@_> with the subroutine call parameters: # before inlining $result = subcall($foo, $bar); # after inlining $result = do { my ($x,$y) = ($foo, $bar); ... # subroutine body here }; As is, the regular expression to handle inlined sub calls only detects + a single list as parameters i.e. \((s[^\n].*)\) which means that a parameter list must begin and end on the same line. + Multiline parameter lists are not supported (but see "Overriding Conventions" be +low). Any EXPR used as subroutine call parameter must be resolvable in the c +ontext where inlining takes place. =head2 Overriding Conventions You may want to provide your own, more sophisticated filtering regexps + according to your coding conventions. To that end, you may pass in the following + named parameters along with their values to import, which will override the +builtins: =over 4 =item callmatch Compiled regular expression (via C<qr> - see perlop) used to match inl +ined subroutine calls. Default: qr{inline\s+(\w+)\s*\(([^\n]*)\)}; This matches the inlined sub's name and its parameter list in $1 and $ +2. =item plainmatch String containing a single C<qr> call which will be eval'ed by the fil +ter. It must contain the string '$sub' which - after being eval'ed -will ho +ld the current subroutine's name during the filtering process. Default: 'qr{\b$sub\s*\(([^\n]*)\)}' This matches plain (without 'subroutine' prefix) calls to subroutines +declared as inline subs. =item declmatch Compiled regular expression (via C<qr> - see perlop) used to match sub +routine declarations or definitions prefixed with the C<inline> keyword. Defau +lt: qr{^inline\s+sub\s+(\w+)\s*(?:;|\{)}ms =item bodymatch String containing a single C<qr> call which will be eval'ed by the fil +ter. It must contain the string '$sub' which - after being eval'ed -will ho +ld the current subroutine's name during the filtering process. Default: 'qr{^sub $sub\s*(\{\n.+?\n\})$}ms' =back Additionaly, you can pass in the keyword 'debug' with a true value, wh +ich will print diagnostics to STDERR. =head1 CAVEATS AND BENEFITS Standard caveats and frowning towards source filters apply. Keywords meaningful inside subroutines may not do what you expect - na +mely C<return>, C<caller> and C<wantarray>. Since the overhead of calling a named subroutine over fully inlined co +de (without even a C<do> block around) is roughly that of calculating seven intege +r reciprocals, most performance benefits are obtained with simple and heavily used su +broutines. For the example in the SYNOPSIS, inlining as C<do> blocks without assi +ngning shows a performance boost of roughly 100% against the same code with subrout +ine calls, while C<do> blocks with assignment of arguments to private variables m +easures as a 50% increase. =head1 SEE ALSO Filter::Util::Call =head1 AUTHOR shmem, E<lt>shmem@cpan.orgE<gt> =head1 COPYRIGHT AND LICENSE Copyright (C) 2018 by shmem This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.20.2 or, at your option, any later version of Perl 5 you may have available. =cut

Using this module, the following

use Inline::Blocks; inline sub capitalize_next; print uppercaseIncrementAsString('a'..'f'), "\n"; sub uppercaseIncrementAsString { my @l = @_; my $ret; $ret .= capitalize_next($_) for @l; $ret; } sub capitalize_next { my ($thing) = @_; uc inline increase($thing); } sub increase { my ($foo) = @_; ++$foo; }

results (via B::Deparse) in

use Inline::Blocks; print uppercaseIncrementAsString(('a', 'b', 'c', 'd', 'e', 'f')), "\n" +; sub uppercaseIncrementAsString { my(@l) = @_; my $ret; $ret .= do { my($thing) = $_; uc do { my($foo) = $thing; ++$foo } } foreach (@l); $ret; } sub capitalize_next { my($thing) = @_; uc do { my($foo) = $thing; ++$foo }; } sub increase { my($foo) = @_; ++$foo; }

What do you think? does that suffice or should we have an inline keyword? Apart of answers to that question, critics are welcome, improvements also, e.g. for the regexps in the regexp variables, their names etc.

perl -le'print map{pack c,($-++?1:13)+ord}split//,ESEL'

Replies are listed 'Best First'.
Re: RFC: Inline::Blocks or inline as a keyword?
by tobyink (Canon) on Jul 30, 2018 at 10:29 UTC

    I'm the author of Type::Tiny which internally does a lot of inlining. I mean, seriously a lot.

    As an example, there is code defined for Int to check a value is an integer, and code defined for ArrayRef to check a value is an arrayref. A lot of Type::Tiny is about efficiently being able to compile those into checks like ArrayRef | Int (check a value is an ArrayRef or an integer), ArrayRef[Int] (check a value is an arrayref of integers), and Int | ArrayRef[Int] (check a value is an integer or an arrayref of integers). If the arrayref is long, you really want to avoid calling a sub to do the integer check for every single element.

    So here are my thoughts on what you've said:

    Your benchmark is a little too simple. You'll notice the "plain" case and the "do" case run at the same speed. This is because Perl is able to optimize the do case to the plain case. If your do block did something like load a lexically scoped pragma or declare lexical variables, it could not be optimized this way and would run slower than the plain case. Though still a lot faster than a sub call. If a do block can be avoided, you'll get better performance.

    Your module implementation is mostly good, but I don't like how you configure it with global variables. What if two modules by different authors try to alter $declmatch? It would be better to allow people to pass these as parameters to import.

    You don't seem to do anything to cover the case where the inlined sub closes over variables which are declared later than when it is called.

    I have a module called Sub::Block which is similar in aim to your module, though takes a pretty different approach. You may be interested in borrowing the _check_coderef function though. Given a coderef, it inspects the optree for the sub body, searching for any use of return or wantarray and throws an error if it finds them. As noted in your documentation, using return in inlined code is bad. (You may also want to note wantarray and caller in your documentation as also being promlematic.

      Your benchmark is a little too simple. You'll notice the "plain" case and the "do" case run at the same speed.

      Of course, but there's that drop for the "do_vars" sub in which the arguments passed to a subroutine are assigned as lexicals inside the "do" block. In that case optimizing away the scope isn't possible, which of course happens with loading pragmas.

      Your module implementation is mostly good, but I don't like how you configure it with global variables. What if two modules by different authors try to alter $declmatch? It would be better to allow people to pass these as parameters to import.

      Thank you. - Indeed that's work to be done yet (and easy enough): making the package globals into package private lexicals as defaults, which can be overridden by named import parameters. This definitely makes more sense. update: done in the OP.

      You don't seem to do anything to cover the case where the inlined sub closes over variables which are declared later than when it is called.

      This is on purpose, because that would get me into trouble, into the egg vs. hen problem: I would need to check for outer frames and inspect the nature and existence of variables in those, which are used inside the inlined block. But only constants in closures are suitable for inlining, dynamic closures aren't. So I would need to compile the whole stuff first to munge the source after the fact... no. This module is purely for textual replacement, think cpp.

      As a related note, perl4 had the -P command line switch, which caused the source being run through cpp. When did that go away in perl 5? and why?

      perl -le'print map{pack c,($-++?1:13)+ord}split//,ESEL'

        The -P switch was deprecated in 5.10 and removed in 5.12. (This was before the current two-stable-versions deprecation cycle.)

        It was replaced with Filter::cpp (though it's only on CPAN, not in core).

      Hi Toby

      > I'm the author of Type::Tiny which internally does a lot of inlining. I mean, seriously a lot.

      May I ask how you solved this?

      > This is because Perl is able to optimize the do case to the plain case.

      Are you sure?

      DB<17> sub test { print do { 5+4 } } DB<18> print B::Deparse->new("-p")->coderef2text(\&test) { use feature 'current_sub', 'say' ... # yadda shortened print(do { # still here 9 }); }

      update

      this is the mentioned do-case

      { use warnings; use strict; my($total); foreach $_ (1 .. $end) { ($total += do { (1 / $_) }); } $total; }

      DB<27> print $] 5.024001

      hence the do-opcode is still present.

      I suppose you meant to say that it has no performance penalty.(?)

      Cheers Rolf
      (addicted to the Perl Programming Language :)
      Wikisyntax for the Monastery FootballPerl is like chess, only without the dice

        Each type constraint knows how to generate a string of Perl code for validating a value, given a variable name for the value.

        For example:

        $ perl -MDevel::Hide=Ref::Util::XS,Type::Tiny::XS -MTypes::Standard=-t +ypes -E'say HashRef->inline_check(q/$myvar/)' Devel::Hide hides Ref/Util/XS.pm, Type/Tiny/XS.pm (ref($myvar) eq 'HASH')

        So the ArrayRef[HashRef] type constraint can use that when generating a longer bit of code to validate an arrayref of hashrefs:

        $ perl -MDevel::Hide=Ref::Util::XS,Type::Tiny::XS -MTypes::Standard=-t +ypes -E'say ArrayRef->of(HashRef)->inline_check(q/$myvar/)' Devel::Hide hides Ref/Util/XS.pm, Type/Tiny/XS.pm do { (ref($myvar) eq 'ARRAY') and do { my $ok = 1; for my $i (@{$myvar +}) { ($ok = 0, last) unless (ref($i) eq 'HASH') }; $ok } }
Re: RFC: Inline::Blocks or inline as a keyword?
by tobyink (Canon) on Jul 30, 2018 at 10:37 UTC

    Oh, and see also GFUJI's macro which takes a similar approach to your module, but not as nice a syntax.

    Also this old github gist of mine which is kind of similar to GFUJI's module (less polished), but uses the Perl 5.14 keyword API instead of source filters.

Re: RFC: Inline::Blocks or inline as a keyword?
by holli (Abbot) on Jul 30, 2018 at 14:43 UTC
      Source filtering? No!

      Quick, throw away your C pre-processor! and along with that, all that autoconf and automake rubble.
      Seriously, why should source filtering be bad for perl, but good for C?

      I have explained in the op why in this case source filtering makes sense, and I see no point against that if it is done sensibly. If I don't do it sensibly, please point out why.

      You seem to have missed Damians latest(?) stroke of genius resulting in Keyword::Declare and PPR.

      I missed them only slightly. I am aware of those beasts, but I didn't delve much into them because I currently haven't any use for them. Please don't compare me to Damian, that's unfair to both of us.

      perl -le'print map{pack c,($-++?1:13)+ord}split//,ESEL'
        I think there's a misunderstanding here, I am not comparing anything. I wanted to express that the Keyword::Declare module is the tool you need to implement your module without source filters or rolling your own perl parsing regexes.


        holli

        You can lead your users to water, but alas, you cannot drown them.

        Despite such vehement discouragement, there's nothing (IMHO) intrinsically wrong with source filtering. It's just requires being very careful when using it. Also, it can make a program it using harder to debug.

        Anyway, I can suggest how you might use Keyword::Declare to accomplish your inlining module. Though with a modified syntax.

        Although Keyword::Declare will let you redefine sub (and other keywords), it won't do what you want. Instead, I suggest adding another keyword, maybe inlinable to mark a subroutine as suitable for inlining. Your handler for inlinable would save the body of the sub for later use by inline, then return the sub definition without the inlinable keyword.

        For inline sub definitions, save the body, then instead of returning the sub definition, use the name to define a new keyword. When perl later encounters the name of the sub, it will be treated as a plug-able keyword. then your handler can inline the routine.

        Caveat: Keywords defined by Keyword::Declare are only recognized at the beginning of a statement. If you want to inline in the middle of a statement, you have to enclose the "call" in a do { } block.

        There might be a plus side to this caveat. Since you know the inlining will always take place at the start of a statement, you might not have to inline it as a do { } block, but as a plain block. This is because if your inlined code isn't "returning" a value, you shouldn't need the do { } block. But if it does "return" a value and you want that value, the "call" will already be inside a do { } block (as long as it is the last (or only) statement in the do { } block).

        Summary: A Keyword::Declare based module may be easier to fully implement, but a source filter version probably will result in a cleaner syntax. The Keyword::API might be able to install keywords that work in the middle of expressions, but is not as easy to use as Keyword::Declare.

        Disclaimer: I have not tested any of this. YMMV.

        Edited to correct grammer errors.

        Seriously, why should source filtering be bad for perl, but good for C?
        In both cases the compiler might report an error where the mistake was inserted, rather than where it actually is. Also in perl, source filters screw up the line numbering (insert two lines in the code, and the line numbers for every compilation message, or warn or die afterwards will be off by two). A call to warn on the last line of your test will indicate an issue on line 35 of a 26 lines file...

        The #line directive can solve most of those issues though, so if you turn

        sub capitalize_next { my ($thing) = @_; uc inline increase($thing); } sub increase { my ($foo) = @_; ++$foo; }
        into
        sub capitalize_next { my ($thing) = @_; uc do { local @_ = ($thing); #line 6 my ($foo) = @_; ++$foo; #line 3 }; } sub increase { my ($foo) = @_; ++$foo; }
        Mistakes in the parameter list (eg increase(£thing)) will be marked as coming from line 3 (the line of the call), and mistakes from the function definition (eg ++£foo) will be reported as coming from inside the function definition.

        local @_ makes it possible to access $_[0] and other values inside @_, while making line numbering easier (if ($foo) = @_ is replaced by ($foo) = $thing, the LHS comes from the function body but the RHS from the call, so where do you report an erreor?), but it probably slows things down... And since $foo = shift; uses @_ inside a function, but @ARGV elsewhere, it might introduce some interresting bugs.

        Also note that the C preprocessor will only expand macros in code, not in comments nor in strings. Try this in your example:

        sub capitalize_next { my ($thing) = @_; # uc inline increase($thing); }
        and you'll get some confusing errors. It's pretty easy to correct that one, but things might get difficult with strings and pod.

Re: RFC: Inline::Blocks or inline as a keyword?
by tobyink (Canon) on Aug 01, 2018 at 14:07 UTC

    I don't know much about source filtering, and perhaps Filter::Util::Call does some whitespace munging, but does this regexp forbid sub declarations from being indented?

    qr{^inline\s+sub\s+(\w+)\s*(?:;|\{)}ms

      Obviously, which is a bug arguably. But then, indented sub declarations are (within my coding standards) closures and such. I will use

      qr{^\s*inline\s+sub\s+(\w+)\s*(?:;|\{)}ms

      if I can be convinced that this is a sensible default for inlineable subs.

      perl -le'print map{pack c,($-++?1:13)+ord}split//,ESEL'

        Indenting some sub definitions is useful for applying a lexical pragma like no warnings qw(redefine) but then again pragmata wouldn't survive inlining. (Though you could set pragma inside the sub.) I do also use them for grouping code occasionally, but the biggest reason to support indented sub definitions is:

        package Foo { sub bar { ...; } }

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlmeditation [id://1219450]
Approved by Athanasius
Front-paged by Discipulus
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others having a coffee break in the Monastery: (4)
As of 2024-04-19 21:53 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found