diff --git a/.gitignore b/.gitignore index a279d1f..cab025d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,6 @@ esy.lock/ *.db *.pem *~ -posts/* -posts/*.blog -posts/*.post -\#* .DS_Store bookshelf.txt site.db* diff --git a/posts/2024.html b/posts/2024.html new file mode 100644 index 0000000..a743ffc --- /dev/null +++ b/posts/2024.html @@ -0,0 +1,59 @@ + + +
++ 2023 was a good year, but now it's 2024. Here's some of my technical plans for 2024. +
++ NOTE: These are in no particular order. +
++ It's not that I hate the following, it's just that they don't really interest me, + or the people behind them do not seem to value people using them (ala Rust). +
+Have a safe and fun 2024.
+ + diff --git a/posts/become-an-engineer.html b/posts/become-an-engineer.html new file mode 100644 index 0000000..a8ce9f9 --- /dev/null +++ b/posts/become-an-engineer.html @@ -0,0 +1,81 @@ + + + ++ This learning track assumes you have a computer built after 2004, with + Debian Linux, FreeBSD or OpenBSD installed. (Windows simply won't do.) +
++ This is the way to becoming a truly great software engineer. I've read all of these books, and I can say that they all manifest within + me a strong sense of confidence, and the ability to adapt to any situation that might come my way in computer science. +
++ You may be reading this list, thinking... what about Java or JavaScript??? Now, I don't think these languages are bad per-se, but I do think + they have the ability to destroy your life. Once you've used Java or JavaScript, there is a good chance you will become closed off from + the rest of the computing world, falling into a world of patterns and objects. You may also find that languages like these have the adverse + effect of having incredibly low skill-ceilings, which compounds the idea of you becoming stuck. A low ceiling, and a low floor, is a prison, not + a tool to conjure the spirits of the computer. +
+ + diff --git a/posts/dancer2-controllers.html b/posts/dancer2-controllers.html new file mode 100644 index 0000000..c5739a6 --- /dev/null +++ b/posts/dancer2-controllers.html @@ -0,0 +1,72 @@ + + + +Published February 26th, 2024.
+
+ Recently I created Dancer2::Controllers, a simple
+ Moose based approach for defining routes in Dancer2. I built this as an attempt to add Spring-Boot esq annotations to
+ Dancer2.
+
+ Perl has a little know feature called "Attributes", which work as some extra arbitrary data appended to
+ the various reftypes in Perl. When you append an Attribute, Perl looks for the MODIFY_<reftype>_ATTRIBUTES in the current
+ namespace, and uses it to decide if that attribute is allowed, and lets you execute some arbitrary code as well. Perl attributes can also take "arguments".
+ These "arguments" actually are just part of the Attribute, and you need to parse the entire Attribute to get the arguments.
+
+ Since Perl Attributes are the closest thing I could find to annotations, I decided a generic Route attribute would be nice.
+ With a Perly looking argument pattern: Route(get => /foo), this feels a Perl version of Spring-Boot's
+ @RequestMapping(value = "/foo", method = GET),
+ which is exactly what I was looking for.
+
+ Dancer2 is built on-top of Moo, unfortunately I couldn't find a nice way to handle Attributes using Moo, and inheritance. So, I settled for adding
+ Moose and the wonderfully helpful MooseX::MethodAttributes dist, which drastically simplified the work I needed to do.
+
The following is an example of how to use Dancer2::Controllers
+
+package My::Controller;
+
+use Moose;
+
+BEGIN { extends 'Dancer2::Controllers::Controller' }
+
+sub home_page : Route(get => /home) {
+ my $app = shift;
+ my $user_name = $app->session('user');
+ return "Welcome back $user_name!";
+}
+
+sub home_page : Route(post => /login) {
+ my $app = shift;
+ my $password = $app->body_parameters->get('password');
+ return "Here is your password: $password";
+}
+
+1;
+
+package main;
+
+use Dancer2;
+use Dancer2::Controllers;
+
+controllers([ 'My::Controller' ]);
+
+dance;
+
+
+ Thanks for reading :)
+ + diff --git a/posts/docker-mod-perl.html b/posts/docker-mod-perl.html new file mode 100644 index 0000000..599f671 --- /dev/null +++ b/posts/docker-mod-perl.html @@ -0,0 +1,144 @@ + + +
+ I've been working under an old Apache mod_perl setup. I wanted to use Mojolicious for some new services,
+ but I needed to stay within mod_perl, so I decided to go all the way and Dockerize it. After scouring the mod_perl documentation,
+ Plack + Mojolicious configs, and finding an image, I hit the ground running and documented my steps.
+
+
+ I love old banners :)
+Thanks to motemen for some nicely pre-configured
+ mod_perl and Apache2 images. We're targetting Perl v5.36, and Apache 2.4.58 here.
+ We'll also add the include statement for our VirtualHost configuration file that we'll make later.
+
+docker run --rm motemen/mod_perl:5.36.0-2.4.58 cat /usr/local/apache2/conf/httpd.conf > httpd.conf
+echo 'Include /usr/local/apache2/conf/mojolicious.conf' >> httpd.conf
+
+
+
+ This will leave you with a httpd.conf that will get picked up by Apache when we run it under docker.
+
+ I couldn't get Apache to run without disabling Event MPM and enabling Prefork MPM. +
+In httpd.conf:
+
+#LoadModule mpm_event_module modules/mod_mpm_event.so
+LoadModule mpm_prefork_module modules/mod_mpm_prefork.so
+
+
+
+ All you need to do is write a Mojo application. In this case, I'm using Mojolicious::Lite but this can
+ be done with the whole MVC setup easily as well. Just make sure you have a sub that returns your app so you can
+ pass it to Plack::Builder.
+
In lib/App.pm:
+
+package App;
+use 5.036;
+use Mojolicious::Lite -signatures;
+
+sub get_app { app; }
+
+get '/' => sub {
+ shift->render( text => "Hello World!" );
+};
+
+1;
+
+
+ Here we write a simple .psgi file that
+ will use our App library and Plack::Builder to
+ create a PSGI application for Plack::Handler::Apache2 to pickup.
+
In app.psgi:
+use 5.036;
+
+use lib 'lib';
+use App;
+use Plack::Builder;
+
+builder {
+ App::get_app->start;
+};
+
+
+ + Very straight-forward, just tell Apache where to find our script, and what handler to use. +
+In mojolicious.conf:
+
+LoadModule perl_module modules/mod_perl.so
+
+<VirtualHost *:80>
+<Location />
+SetHandler perl-script
+PerlResponseHandler Plack::Handler::Apache2
+PerlSetVar psgi_app /app.psgi
+</Location>
+</VirtualHost>
+
+
+
+
+ Now all thats left is making a Dockerfile.
+ We'll use one of motemen's images,
+ and copy all of the pre-reqs over, install some libraries, you know, Docker stuff.
+
In Dockerfile:
+
+FROM motemen/mod_perl:5.36.0-2.4.58-2.0.13
+COPY app.psgi /app.psgi
+COPY lib /usr/local/apache2/lib
+COPY httpd.conf /usr/local/apache2/conf/httpd.conf
+COPY mojolicious.conf /usr/local/apache2/conf/mojolicious.conf
+RUN apt-get update -y && apt-get install -y wget make build-essential
+RUN cpan -iT Plack Mojolicious
+EXPOSE 80
+CMD ["httpd-foreground"]
+
+
+
+Now we can make it go!
+
+$ docker build . -t mod-perl
+$ docker run -p 80:80 mod-perl
+$ curl http://localhost:80/
+
+"Hello World!"
+
+
+ You can make this auto-reload if you use a docker volume for lib/ against /usr/local/apache2/lib/
+ An example is available on my github. +
++ I reccommend GNU Emacs (not Spacemacs, or Doom Emacs) to everyone. If you're just starting to program + then you're at an even bigger advantage if you pickup Emacs early. If you're a battle hardened developer + you should still be using Emacs, though it may be harder to pickup, assuming you've been using modern "IDE's" + for most of your career. I also recommend Emacs to pretty much anyone that processes text often. +
++ Emacs provides the perfect balance of customization, to effectiveness of any editor. I think that it's foolish + to use tools that don't let you mold them to your idea of how to use them. Software tools aren't like traditional + tools like Hammers and Pipe-wrenches, and it's foolish to use them in this way. With Emacs a Secretary, Software Engineer (obviously), + Math Student, Professor, Novelist, etc, can all find ways to implement Emacs into their day-to-day. + Emacs is a tool for empowering people. It's not just a text-processor (though it's really good at that), it's also a journal, + a lisp interpreter, and really, anything you want. +
++ I'm going to focus on Software Developers for the rest of this, seeing as it's who I can relate too most. + Software Developers often get stuck in ruts, I truly think Emacs is the tool to pull you out and keep you out + of said ruts. Emacs will kick you in the rear, and make you cry, and force you to work in a subpar fashion for a long time, + the GNU is not an easy beast to tame. But once you've put in the work, and elevated yourself upon it's reigns, the power you + attain is so incredibly wizard-like that you may very well consider wearing a pointed hat, and robes, at work. +
++ Currently, I use Emacs for everything at work. Compiling, code editing, file-system stuff, docker, git, documentation, etc. + I've used other editors and nothing, comes even close to Emacs. Vim users will probably recoil, but Vim makes you context + switch so often between the terminal and your editor that I really think it doesn't even belong in the same category as Emacs. + What I would recommend to the Emacs novice, is to focus on setting up your workflows 1 flow at a time. For instance, say you want + to work on Perl code. Setup all the Perl related stuff in one-shot and don't worry about making your configuration super ergonomic + and extensible. Just make it work for Perl. Then say, you get a job in Scala, do the same thing, but refactor your configuration + to enable both Perl and Scala nicely, rinse and repeat until your hands bleed. That's really all there is to it, as you find things + you want to improve, improve them one-by-one. Don't rush in and expect to setup a perfect IDE for every language and every workflow. + Emacs really is what you make of it, but you can't really know what to make of it, until you identify the problems you want to solve. +
++ A good way to think of Emacs, is, it's the instrument equivalent of text-processors. +
++ Global variables, and global state in general have been under fire for as long as I can remember. + The argument is, you couple, and introduce unpredictability through managing global variables + since "anyone can update them". This makes sense in a cosmological world, filled with people + who WANT to write bad code. It's not the global variables that make code bad, it's how they're used. + We've spent many years inventing abstraction after abstraction, that could just be boiled down to global variables, + and global constants. In the end, applications will always have global state, why make it so dang difficult? +
++ Globals are simple, so simple, that even the thought of using them might turn people off. It's hard to accept + that the simplest answer is always the best one, but it almost always is. 99% of the code I run into, is complex for the sake of "reusability", "readability", or + "scalability", but in the end none of these come to pass. You'll see interfaces, and abstract classes defined for single implementations, + horribly generic code written in obscure places, and a mess of a file structure with hundreds of files. Do you really need a generic interface for + each of your database tables, or what about a "service" for each "domain"? In reality, the most readable, resuable, and scalable code, is the code that + compiles well, and avoids mentally expensive abstraction. If I have to walk through the inheritence graph to figure out what something is doing, it's already neither + scalable, or readable. +
+
+ You'll often see global variables hidden behind some "global context" like abstraction. This is down right deplorable, instead, hide your globals behind the simplest
+ abstraction on the market, the function. GetDatabaseHandler() is much simpler to reason about than GlobalContextHolder.getSingleton().getContext("database").
+ These abstractions also, are prone to error, for instance if you use a hash-table to lookup values with a string key, what happens if I miss type the string? The compiler
+ won't catch that for you, and you'd be adding a lot of extra instructions just to access some state somewhere (which at its fastest is a pointer dereference). Another common way to do this,
+ is to pass a class object, .class in Java. However, this is even worse. Typically, a language will use reflection which or runtime type checks which can be extraordinarily slow.
+ To compound this, you also have no gurantees on the existence of the values, for instance, if you want a DatabaseHandler, and you make the request for it, you have no idea
+ if that is actually the right name or class until runtime. Contrast that with a function that will be picked up by your LSP or produce a compile-time error message.
+ Global contexts, and contexts in general are a huge mess of slow, hard to optimize code, you should avoid them at all costs.
+
+ As mentioned earlier, global state should exist one layer under a function. There is absolutely nothing wrong with running a database.InitializeDatabase() function, to
+ initialize your globals at startup, or having some sort of lazy-init built into your global accessor function. Keeping things simple is how you keep them readable, and scalable.
+ What's funny, is that's pretty much it.
+
+ Another thing I recommend is not accessing your globals inside of your functions, instead, pass them as arguments. This is just good-practice in general, your functions should + do their best to be as pure as possible. This also makes your code truly re-usable and adaptable, since you may have multiple different global variables with the same types. + This also makes your code easy to test, since it's not relying on any outside values existing or not. +
+
+ Let's compare some code. In the first example, we'll do the traditional GlobalContextHolder OOP style,
+ where we literally do our best to make it look like we're not working on global variables. In the second example, we allow our globals to exist,
+ but instead of accessing them in a foolish way, we accept them as parameters.
+
Example 1:
+
+
+public void saveUser(User u) {
+ // Where did this guy come from? Idk, it's probably defined in some XML file somewhere :)
+ DatabaseHandler dbh = GlobalContextHolder.getContext("database");
+ dbh.table("users").save(u);
+}
+
+// Main.java
+public static void main(String[] args) {
+ User user = new User("Bob", 23);
+ saveUser(user);
+}
+
+
+ Example 2:
+
+
+// Database.java
+private static DatabaseHandler DBH;
+
+public static DatabaseHandler getDatabase() {
+ if (DBH == null) {
+ DBH = DatabaseHandler.newConnection("mysql://foo@bar:foo.com:3306");
+ }
+
+ return DBH;
+}
+
+// Main.java
+public static void saveUser(User u, DatabaseHandler dbh) {
+ dbh.table("users").save(u);
+}
+
+public static void main(String[] args) {
+ Database.initialize();
+ User user = new User("Bob", 23);
+ saveUser(user, Database.get());
+}
+
+
+ + Overall, this drastically reduces the mental overhead of the system, allowing for simple unit-testing (if you want), and + a simplistic approach to scaling, since you don't have to worry about costly abstractions that don't actually simplify anything. +
+ + diff --git a/posts/jpa-hibernate-stupid-performance.html b/posts/jpa-hibernate-stupid-performance.html new file mode 100644 index 0000000..631dc02 --- /dev/null +++ b/posts/jpa-hibernate-stupid-performance.html @@ -0,0 +1,56 @@ + + + ++ JPA is not performant. Mainly because of things like batching, and this incessant idea that + querying relationships lazily is somehow fast, its literally just deferring the work, which makes sense + if you potentially don't want to use the data, but most people want that data anyways, or serialize it. +
++ I experienced this recently with a massive data set, this database was 1 core table of 4 million rows, followed by multiple child One-To-Many table + of close to 10 million rows, and a single One-to-One with ~4 million rows as well. To use the service built on top of this massive data, you had to search + with JPA search specifications, which turns out also to be insanely slow. The benchmark starts us at 15 or so minutes, which is obviously unacceptable. +
+
+ By enabling logging.level.org.hibernate.SQL=DEBUG and logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
+ you're able to see the ridiculous number queries JPA is executing, they're un-countable, there's so many!! How can we fix this?
+
+ If you're thinking of using @Batch(100), it could work, only issue is JPA will batch each relationship, so each of our tables will still require
+ n/100 calls where n is the number of rows with relationships. So theoretically a 100x speed bump right? Well, it's actually more
+ like a 33x performance bump, since you still have the overhead of making the database connections, and need to get a lock every time you select. Doing this we moved to around
+ 3-5 minutes depending on load per large call. This however, was still unacceptable.
+
+ What if instead we just forced JPA to use OUTER JOIN? Isn't that exactly what the ANSI SQL wizards would tell you to do?
+ Obviously, this is the solution, one single connection, 1 lock, and let the B-Tree optimizations inside of the database handle all of this ugliness for us.
+ To do this you need to use something called @EntityGraph
+ on your JPA repository, what this does is forces JPA to use a OUTER JOIN
+ to join relationships. This even works on nested entities, say I have Entity A with a child B which also has a One-to-Many on C, all you need to do
+ is add the relationship to the entity graph with the @NamedEntityGraph annotation, then provide it with a few nice args like name,
+ and attributeNodes which will point to your sub entity. Then finally on your JPA repository call you stitch the entity graph back together.
+
+
+@Repository
+public interface ARepository extends JpaRepository<A, UUID>, JpaSpecificationExecutor<A> {
+ @Override
+ @EntityGraph(attributePaths = { "bs", "B.cs" }) // Where B's are children of A's, and C's are children of B's.
+ Page<A> findAll(@Nullable Specification<A> specification, Pageable pageable);
+}
+
+
+
+ After applying these, the query time went down from 15 minutes to ... *drum-roll* ... 3 seconds. So please, for the love of all that is computationally efficient, force your JPA
+ to use OUTER JOIN, or even roll your own SQL, ORM's really are one of the biggest trade-offs, and they really can bite you. This is why tend to
+ stay in the DBI universe when I write Perl. It's funny shocking Ruby on Rails and Java developers with my ~15ms queries :D
+
+ I've been using evil-mode for almost 4 years at this point, and Vim keybinds even longer.
+ However, I think it's almost time for me to change. In 2022 I spent the later half of the year just on default, vanilla emacs keybinds.
+ This was really enjoyable and let me learn more about how the puritans do Emacs, but in general,
+ I just felt a lot slower. So I came back to evil-mode, and have been using it daily ever since.
+ But with some recent annoyances in Evil, and having to almost double my config size to use Evil integrations with other packages,
+ I've been thinking of using Vanilla again, or exploring some of the other modal plugins like: meow,
+ Xah-Fly-Keys, or god-mode (don't love the name of this one :P).
+ So, to the disdain of my productivity, I am going to try to use each of these for a week at a time, and report back each week with an update.
+
+ I will be comparing each mode on the following all scored out of 10: +
+evil-mode's rating
+ Here is my review of evil-mode:
+
+ Overall I do like evil-mode, and i'm very productive with it, but I find that a lot of little issues pop-up on a day to day basis
+ that could be ironed out. Things like not having re-do automatically working. At the end of the day I use Emacs for both, speed, but also,
+ for extensability, something that I think evil-mode restricts.
+
Published March 7th, 2024
++ Earlier this week, + Bleacher Report + was bought out and forced to re-write their Elixir/Erlang backend in something "more-mainstream" by the + buying party (Warner), this is a HUGE mistake, and an example of how main-stream thought has eroded the foundations of software engineering. Bleacher Report had used + Elixir/Erlang since at least 2017, + when their case-study of going from 150 servers to 5 via converting their Ruby code to Elixir/Erlang was published. + All of this resliency, and engineering effort is now going to be rewritten in none other than JavaScript, which of course will be a tremedous effort. +
++ So why did they do it? Well, they had lay-offs, and then they needed to re-fill the seats they laid off a few years later. Instead of leveraging this "gold card" + that is a niche language, Warner decides that it will be too hard to fill N seats. Which is also another issue in the software world, the "butts in seats typing" + phalacy that more will get done. Anyways, what Warner could have done, is use this fantastical power bestowed by a niche language, and got the best developers + in that niche technology. If they hired 15 Node devs for 100k a year each, they could afford 5 Erlang masterminds for 300k a year each (and then you wouldn't need a + rewrite!) The other option is to hire a few Erlang devs and have them train other devs which could be an effective cost-cutting measure as well, without + having to rewrite your codebase. +
++ I've been asked many times by developers, "should I learn X, it's so niche but it looks cool", and to every single one of those questions, I shout "YES!!!". + Learning is ALWAYS good. But also, if you get really good at these niche technologies, you can become a SME (subject matter expert), and get hired + by a business looking for people like you. Jane Street is notorious for using OCaml for literally everything. Yaron Minsky, mentions at Jane Street states + that while there aren't many OCaml developers, the small set that does exist is typically really good, and a grade above the usual Python, Java, JavaScript developer + (you'll choke on your coffee when you read Jane Streets developer salaries as well). +
++ When starting a new project I think you should consider using a niche language. + You learn a lot, but also when it comes time to scale your idea, and you need to hire a partner or two, + you'll be greatful you chose it, when extremely talented and passionate individuals send you their resume solely because you chose a backend written in Haskell, Scala, + Perl, OCaml, Elixir, Clojure, Lua, etc. This is what happened for NoRedInk, after writing their original MVP in Ruby, they ended up using Haskell and Elm for new + services, and now, they're one of the most well known Functional Programming shops, and they've managed to hire some brilliant people, like the creator of Elm himself! +
+ + diff --git a/posts/perl-oo-action-benchmark.html b/posts/perl-oo-action-benchmark.html new file mode 100644 index 0000000..213a658 --- /dev/null +++ b/posts/perl-oo-action-benchmark.html @@ -0,0 +1,558 @@ + + + + +
+ I've spent a lot of time working in projects that use the Action pattern (also called the Command pattern).
+ Now, regardless of how i've land on OO vs Functional in the last 5 years, there has always been a question I have yet to answer: "How much does this abstraction cost,
+ and more importantly, how much does object instantiation actually cost?" (since the action pattern has you instantiating A LOT of objects)
+
+ So I set out to benchmark, Perl OO, using the traditional Module OO, Corinna, Moo and Moose. But I thought it may also
+ provide some value to compare these findings to other languages, namely ones in the same sort of realm that Perl has lives in: Ruby and Raku (formerly Perl 6).
+
+ The benchmark is fairly simple, since the goal here is to test the actual instantiation costs, so the method associated with the implemented action is very simple.
+ An Action object has two members, name and age. It's execute method returns a Map/Hash/Dictionary, with the following keys
+ { name => self.name, age => self.age }. Basically just accessing the instantiated objects age and name attributes, and associating them to
+ keys of the same name.
+
+ One-million Action objects will be instantiated in a O(n) loop, and executed, then destroyed all within the same scope. Like so:
+
+
+my $name = 'Tim';
+my $age = 12;
+for ( 1 .. 1000000 ) {
+ my $user = Action->new( name => $name, age => $age )->execute();
+}
+
+
+ + This will be performed one-hundred times, then, the average of the times taken to do the one-million iterations will be the benchmark result. + It is also important to note that the initial load-time of the language run-times is included in the calculation, though they are mostly negligble especially + in the Perl case, and the actual instantiation takes the vast-majority of the time (close to 99% on average). +
++ This benchmark was performed on an Intel i7 12700KF, using DDR5 memory. +
+| Language | +Performance | +
|---|---|
| + Perl Module OO + | ++ 454.627ms + | +
| + Perl Module OO + Types + | ++ 505.276ms + | +
| Corinna | +564.411ms | +
| + Moo + | ++ 960.03ms + | +
| + Moo + Types + | ++ 2308.226ms + | +
| + Moose + | ++ 11420.455ms + | +
| Moose + Immutable | +1182.109ms | +
| + Moose + Types + | ++ 13649.37ms + | +
| Ruby | +167.627ms | +
| Raku (MoarVM start-up subtracted) | +801.947ms | +
+ Using the traditional Perl approach to OO using ref's and modules, is by far the fastest. +
+
+
+package Action;
+
+use strict;
+use warnings;
+
+sub execute {
+ my $self = shift;
+ return +{ name => $self->{name}, age => $self->{age} };
+}
+
+sub new {
+ my ( $class, $name, $age ) = @_;
+ my $self = { name => $name, age => $age };
+ return bless $self, $class;
+}
+
+1;
+
+package main;
+
+use strict;
+use warnings;
+
+my $name = 'Tim';
+my $age = 12;
+for ( 1 .. 1000000 ) {
+ my $user = Action->new( $name, $age )->execute();
+}
+
+
+
+ This implementation is alright, it's fast, but it doesn't provide any validations, which is what I assume most OO frameworks like Moo and Moose do, especially
+ if you use modules like Type::Tiny.
+
+
+PERL TIME : 454.627ms
+
+
+
+ Considering my assumptions about other OO libraries I decided to add a few checks to the Module OO implementation,
+ using Scalar::Util we can test if age is a number, and also ensure the defined-ness of our
+ attributes using the built-in defined.
+
+
+package Action;
+
+use strict;
+use warnings;
+use Scalar::Util qw(looks_like_number);
+
+sub execute {
+ my $self = shift;
+ return +{ name => $self->{name}, age => $self->{age} };
+}
+
+sub new {
+ my ( $class, $name, $age ) = @_;
+
+ if ( !defined $name || !defined $age ) {
+ die 'name and age, should be defined.';
+ }
+
+ if ( !looks_like_number($age) ) {
+ die 'age should be a number.';
+ }
+
+ my $self = { name => $name, age => $age };
+ return bless $self, $class;
+}
+
+1;
+
+package main;
+
+use strict;
+use warnings;
+
+my $name = 'Tim';
+my $age = 12;
+for ( 1 .. 1000000 ) {
+ my $user = Action->new( $name, $age )->execute();
+}
+
+
+ This only came at a net-cost of around 50ms extra on average, which is surprisingly fast.
+
+
+PERL + TYPES TIME : 505.276ms
+
+
+ + Corinna is a new OO system added to Perl 5.38 but still marked as experimental. It performs very well compared to the features it provides, + but it is definitely not something you'll see in the wild too often. +
+
+
+use feature 'class';
+no warnings;
+
+class Action {
+ field $name : param;
+ field $age : param;
+
+ method execute () {
+ return +{ name => $name, age => $age };
+ }
+}
+
+my $name = 'Tim';
+my $age = 12;
+for ( 1 .. 1000000 ) {
+ Action->new( name => $name, age => $age )->execute();
+}
+
+
+
+ I had to disable warnings on this because I wasn't sure what flags I needed to set in the feature or the experimental,
+ also the lack of documentation definitely hurt, it took me a while to figure out I had to use feature 'class';. For some reason I thought
+ the feature flag was corinna or something.
+
+
+CORINNA TIME : 564.411ms
+
+
+
+ Moo is a "Minimalist Object Orientation" for Perl. It's what I've used the most, and it performs quite well for all of the
+ features it gives you. However, there is definitely a cost, one that a lot of developers ignore. I've done two implementations here, one with
+ Type::Tiny, which adds optional type-checking to attributes, and one without. The difference was quite compelling!
+
Type::Tiny:
+
+
+package Action;
+
+use Moo;
+use namespace::clean;
+
+has name => (
+ is => 'ro',
+ required => 1
+);
+
+has age => (
+ is => 'ro',
+ required => 1
+);
+
+sub execute {
+ my $self = shift;
+ return +{ name => $self->name, age => $self->age };
+}
+
+1;
+
+package main;
+
+use strict;
+use warnings;
+
+my $name = 'Tim';
+my $age = 12;
+for ( 1 .. 1000000 ) {
+ my $user = Action->new( name => $name, age => $age )->execute();
+}
+
+
+ With Type::Tiny:
+
+
+package Action;
+
+use Moo;
+use Types::Standard qw(Int Str);
+use namespace::clean;
+
+has name => (
+ is => 'ro',
+ isa => Str,
+ required => 1
+);
+
+has age => (
+ is => 'ro',
+ isa => Int,
+ required => 1
+);
+
+sub execute {
+ my $self = shift;
+ return +{ name => $self->name, age => $self->age };
+}
+
+1;
+
+package main;
+
+use strict;
+use warnings;
+
+my $name = 'Tim';
+my $age = 12;
+for ( 1 .. 1000000 ) {
+ my $user = Action->new( name => $name, age => $age )->execute();
+}
+
+
+
+ To my shock, Type::Tiny degrades performance by close to 300%. However, the more I looked into Type::Tiny, the more it made sense.
+ Type::Tiny, to enforce types uses a lot of complex meta-programming, and ref checks, that add up to a ton of extra operations. Perhaps it would be
+ worth using B::Concise or B::Deparse to see how many more operations this actually adds, but thats something for another day.
+
+
+MOO TIME : 960.03ms
+MOO + TYPES TIME : 2308.226ms
+
+
+
+ Moose is a heavyweight, industrial purpose OO framework for Perl, at which Moo derives from. It is big, and heavy, and unfortunately,
+ really slow for tasks like this. After starting this benchmark, I thought that the slowness was coming from loading the module between runs, but after timing the average load time,
+ the time spent was negligble (31ms at most).
+
+ Like in the Moo benchmark, I did two implementations, one using Type::Tiny, and one not.
+
+
+package Action;
+
+use Moose;
+use namespace::clean;
+
+has name => (
+ is => 'ro',
+ required => 1
+);
+
+has age => (
+ is => 'ro',
+ required => 1
+);
+
+sub execute {
+ my $self = shift;
+ return +{ name => $self->name, age => $self->age };
+}
+
+1;
+
+package main;
+
+use strict;
+use warnings;
+
+my $name = 'Tim';
+my $age = 12;
+for ( 1 .. 1000000 ) {
+ my $user = Action->new( name => $name, age => $age )->execute();
+}
+
+
+ With Type::Tiny:
+
+
+package Action;
+
+use Moose;
+use Types::Standard qw(Int Str);
+use namespace::clean;
+
+has name => (
+ is => 'ro',
+ isa => Str,
+ required => 1
+);
+
+has age => (
+ is => 'ro',
+ isa => Int,
+ required => 1
+);
+
+sub execute {
+ my $self = shift;
+ return +{ name => $self->name, age => $self->age };
+}
+
+1;
+
+package main;
+
+use strict;
+use warnings;
+
+my $name = 'Tim';
+my $age = 12;
+for ( 1 .. 1000000 ) {
+ my $user = Action->new( name => $name, age => $age )->execute();
+}
+
+
+
+ Just like Moo with types, Type::Tiny hits the final Moose result by around 2000ms overall.
+
+ Amendment: After publishing my original findings I was alerted to a setting you could apply to Moose classes that make them immutable. + Immutable often means faster in-terms of creation but slower in terms of manipulation, but since this workload spends most of its time creating, and none of its time editing, + this produced an incredible 10x speed improvement. +
+
+
+__PACKAGE__->meta->make_immutable;
+
+
+
+
+MOOSE TIME : 11420.455ms
+MOOSE + TYPES TIME : 13649.37ms
+MOOSE IMMU TIME : 1182.109ms
+
+
+ + I also did a quick Ruby implementation, since its a "true" Object-Oriented language, I assumed it would have + a lot of optimizations built-in for this sort of work, and based on its runtime, that seems to be the case. +
++ Also, note, this was on Ruby 3.1.2, not Ruby 3.3 with YJIT, so this could theoretically be faster. +
+
+
+class Action
+ def initialize(age, name)
+ @age = age
+ @name = name
+ end
+
+ def execute
+ { name: @name, age: @age }
+ end
+end
+
+age = 12
+name = 'Tim'
+1000000.times do
+ user = Action.new(age, name).execute()
+end
+
+
+
+
+RUBY TIME : 167.627ms
+
+
+ + Finally, we have Raku, which unfortunately doesn't do too well. To make this a little more competitive, + I subtracted the run-time of MoarVM from the end-result. Though, I think Raku's implementation is the simplest to + understand in terms of langauge design, so it gets a +1 from me for that. +
+
+
+class Action {
+ has $.name;
+ has $.age;
+
+ method execute {
+ return %(name => $!name, age => $!age);
+ }
+}
+
+my $name = 'Tim';
+my $age = 12;
+for 1..1000000 -> $ {
+ my $user = Action.new(name => $name, age => $age).execute();
+}
+
+
+
+
+
+RAKU TIME : 801.947ms
+
+
+
+ Object instantiation has a cost. A lot of people ignore this, especially when using heavyweight libraries like Moose.
+ You can put this into practical terms simply, using basic Perl module OO you effectively double the number of objects of this type you can create and execute a
+ method on given any time-frame compared to Moo without types, and twenty-two times more objects versus Moose without types.
+ But these abstractions exist for reasons, and they are used for a reason.
+
+ Moo and Moose have their places, especially in web-applications where the number of instantiations is low, and the expensive
+ operations happen during side-effects like database operations.
+ This also goes without mentioning some of the great features modules like Moo and Moose provide.
+ It is valuable to know that if you need something to perform well, Moo and Moose
+ may incur overhead you hadn't previously considered. This is compounded when enforcing types.
+ For hot-code that will be hit repeatedly within a short amount of time, this can drastically effect performance, think of things like a landing page, or a search-bar API.
+
+ Corinna, is a very nice prospect, it offers a lot of great things from Moo and Moose but seems to
+ exalt far less of a performance penalty. It will definitely be on my radar in the future, especially when the documentation improves.
+
+ Another approach is to simply use functions, which of course will perform the best. But, it's hard to argue to do this unless performance is the number one goal, + especially on codebases that follow this pattern. +
++ +
+ +