Perl, Inline::CPP and the need for speed (sometimes).

ibrierley

Ian

Posted on March 13, 2023

Perl, Inline::CPP and the need for speed (sometimes).

TLDR;
V1 Perl original regex character strip loop: 11 Seconds
V2 Perl manual charatcer strip loop: 40 Seconds
V3 Perl + Inline::CPP character strip loop: 0.92 Seconds
/TLDR

Summary: A dabble with Perl and the great Inline::CPP module to speed up a bit of performance with a bit of old code.

Inline:CPP is a Perl module which makes it really easy to incorporate bits of C++ with Perl. (Why aren't more people using this! You can even create standalone modules without the need for Inline::CPP)

You can find it here

So, I've been trying to optimise some bits of Perl code recently. We all know premature optimisation in code is evil, but there are times when its worth benchmarking your code and sometimes it's surprising where the slow downs occur. Whilst Perl is fantastic for speed of programming, like any language, sometimes we grab the wrong solution or things just grow without checking in on our code.

So, we do a lot of text processing, this IS what Perl IS good for isn't it ? But sometimes we get lazy keeping an eye on our code, as long as it works.

So the original idea of this code (to be fair it was written about 15 years ago by someone else!), was to sanitise some text, strip out excess whitespace (but leave one) and make lower case, for comparing duplicates once all the clutter (punctuation, html etc) was removed.

This is naturally a simplified example.

V1

# get rid of non-alphas and excess spaces and make lower case
sub perl_strip {
        my ( $string ) = @_;
        $string =~ s/[^0-9a-zA-Z]/ /g;
        $string =~ s/ +/ /g;
        $string =~ s/^ +//;
        $string =~ s/ +$//;
        lc $string;
}
Enter fullscreen mode Exit fullscreen mode

Ok, immediately, I suspect there's some better way within regex itself, but as we add more clauses it gets messy (simplified example again).

Anyway, after benchmarking, this looked pretty slow, and it kinda felt dumb looping over the same text even with regex repeatedly. Regex deep down was written in C afaik, so it's fast, but this approach doesn't feel optimal.

So lets try and come up with a general solution that doesn't do that.

At this point, out of interest, I actually asked ChatGPT (I find it good for suggested approaches), and with some coaxing to what was on my mind (it tried the regex only route, but failed with exactness), "we" came up with a simple loop with a check on the previous character/state...

V2

my ($str) = @_;

my $result = '';
my $last_char_was_space = 1;

for my $c (split //, $str) {
    if ($c =~ /[a-zA-Z0-9]/) {
        $result .= $c;
        $last_char_was_space = 0;
    } elsif (!$last_char_was_space) {
        $result .= ' ';
        $last_char_was_space = 1;
    }
}
return lc $result;
Enter fullscreen mode Exit fullscreen mode

Ok, maybe that makes sense logically, but is it faster ? Actually, no. I'm guessing (but don't know), maybe the optimised regex code easily makes up for any improvement in approach.

However, What about if we spend a little time optimising that loop with some C++, it's pretty straightforward and some Perl genius at https://github.com/daoswald/Inline-CPP has made it easy to merge C++ with Perl (note RPerl I think also uses Inline::CPP). Install as usual via

cpan Inline::CPP
Enter fullscreen mode Exit fullscreen mode

So, I've fluffed about with Inline::CPP before a little (not a lot! So let me know flaws here), and thought this was a prime candidate. After all, we do this processing A LOT on a lot of text, ALL the time.

So next step, get my CPP hat (erm ok, well I don't have one, but I can dig out the basics)...and have a play with Perls Inline::CPP

First of all the clutter in CPP, it's a one off, so I don't care too much, setting up the basics is not too complex, but you may need a little digging on some setups and compilers (you will need a C++ compiler on your system).

# set up basic Perl CPP compile/lib options
use Inline CPP => config => typemaps => './typemap'; ## cppxs will look for typemap if you use that
use Inline CPP => config => ccflags => '-Wall -c -std=c++11 -I/usr/local/include';
use Inline CPP => config => inc => '-I/usr/local/include';
use Inline CPP => config => cc => '/usr/bin/g++';
use Inline CPP => <<'END';

#define extract_string_from_scalar_value SvPV_nolen
#define set_string_value_of_scalar_value sv_setpv
Enter fullscreen mode Exit fullscreen mode

Now some C++..it's doing exactly the same logic as the last Perl example, just with C++, then we call it from Perl as a normal Perl subroutine.

V3

// We will call cpp_strip from Perl!! All the subs are exactly the same logic as the Perl code

#include <string>
#include <cctype>
#include <iostream> 
#include <algorithm>
#include <cstring> // for access to std::strlen

// so we know how to convert std::string to Perl strings, see lower down
typedef std::string cppstring;

cppstring cpp_strip(cppstring str) {
        bool last_char_was_space = true;

        std::string result;
        result.reserve(str.size());

        for (char c : str) {
                if (isalnum(c)) {
                        result += char(tolower(c));
                        last_char_was_space = false;
                } else if (!last_char_was_space) {
                        result += ' ';
                        last_char_was_space = true;
                }
        }

        return result;
}

Enter fullscreen mode Exit fullscreen mode

and I call it within Perl later as

$s = cpp_strip( $string );
Enter fullscreen mode Exit fullscreen mode

I also have a typemap file which is referenced earlier as

use Inline CPP => config => typemaps => './typemap';

Note the two statements I had earlier. I did this just to make it easier whenever I read the code, you could just use SvPV_nolen & sv_setpv but I find that messy as it's not clear to me their intention and not the intent to go into that here. We need a typemap just to know how to convert from Perl types to and back from C++ (or other language) types.

#define extract_string_from_scalar_value SvPV_nolen
#define set_string_value_of_scalar_value sv_setpv
Enter fullscreen mode Exit fullscreen mode

TYPEMAP
  cppstring T_CPPSTRING

INPUT
T_CPPSTRING
        $var = ($type)extract_string_from_scalar_value($arg)

OUTPUT
T_CPPSTRING
        set_string_value_of_scalar_value($arg, $var.c_str());
Enter fullscreen mode Exit fullscreen mode

I call this later in Perl as

 $s = cpp_char_strip( $string );
Enter fullscreen mode Exit fullscreen mode

Benchmarks!

So, I looped over the code repeatedly using a largeish lorem ipsum string 100,000 times...benchmarks as follows.

V1 Perl original regex loop: 11 Seconds

V2 Perl manual strip loop: 42 Seconds

V3 Perl + Inline::C++ string loop: 0.92 Seconds

Final thoughts

I do feel like one of Perls strengths is the ability to combine different languages for performance and flexibility and should be used more often!

I'm also interested in using Rust and Golang, probably using FFI to integrate with Perl. I feel like there are a lot of possibilities with mixing languages we don't often explore (we can also use this for any APIs Perl doesn't have solutions for, but other languages do), and I'm hoping to have a dig into that at some point!

If anyone can do it in pure regex, I'm very interested how it will perform.

Full code, including benchmarks, and an even slightly faster (but more complex) solution can be found here

Note: We can also use InlineX::CPP2XS to convert the C++ code to a Perl XS module to include, if you then want to remove the dependency on Inline::CPP.

💖 💪 🙅 🚩
ibrierley
Ian

Posted on March 13, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related