karlgoethebier has asked for the wisdom of the Perl Monks concerning the following question:

In my basic multithreading exercises i use this unfortunate placeholder sub for simplification:

# this is still a fake! sub mandelbrot { int rand 20; }

But in reality i use a stolen solution (for the first).

For making this stuff my own (AKA understanding it) i tried to refactor it:

#!/usr/bin/env perl use strict; use warnings; use Imager; my $width = 320 * 10; my $height = 240 * 10; my $image = Imager->new( xsize => $width, ysize => $height ); my $black = Imager::Color->new( 0, 0, 0 ); my @palette; my $iterations = 20; for ( 1 .. $iterations ) { my ( $r, $g, $b ) = map { int rand 255 } 1 .. 3; push @palette, Imager::Color->new( $r, $g, $b ); } for my $x ( 0 .. $width - 1 ) { for my $y ( 0 .. $height - 1 ) { my $re_c = ( $x - 3 * $width / 4 ) / ( $width / 3 ); my $im_c = ( $y - $height / 2 ) / ( $width / 3 ); my ( $re_z, $im_z, $color, $test, $div ) = (0) x 5; while ( !$test ) { my $old_re_z = $re_z; $re_z = $re_z * $re_z - $im_z * $im_z + $re_c; $im_z = 2 * $old_re_z * $im_z + $im_c; ++$color; ( $test, $div ) = ( 1, 1 ) if $re_z * $re_z + $im_z * $im_z > 4; ( $test, $div ) = ( 1, 0 ) if $color == $iterations; } ($div) ? $image->setpixel( x => $x, y => $y, color => $palette[$col +or] ) : $image->setpixel( x => $x, y => $y, color => $black ); } } my $file = qq(mandelbrot.bmp); $image->write( file => $file );

I think this doesn't look so bad and it performs slightly better than the original.

But understanding the underlying algorithm isn't so easy for me.

So i thought renaming the names of some of the variables could make my life a bit easier, but I'm still struggling with some.

I wonder what would be "better" names for $test and $div. And doesn't $old_re_z look ugly?

Thank you very much for any hint.

Update: Perhaps a first presentable result:

Made with a little help of some friends ;-)

#!/usr/bin/env perl use strict; use warnings; use Imager; my $width = 1280; my $height = 1024; my $image = Imager->new( xsize => $width, ysize => $height ); my @palette = Imager::Color->new( 0, 0, 0 ); for ( 1 .. 20 ) { my ( $r, $g, $b ) = map { int rand 255 } 1 .. 3; push @palette, Imager::Color->new( $r, $g, $b ); } for my $x ( 0 .. $width - 1 ) { for my $y ( 0 .. $height - 1 ) { $image->setpixel( x => $x, y => $y, color => $palette[ mandelbrot( $x, $y ) ] ); } } my $file = qq(mandelbrot.bmp); $image->write( file => $file ); sub mandelbrot { my ( $x, $y ) = @_; my $re_c = ( $x - 3 * $width / 4 ) / ( $width / 3 ); my $im_c = ( $y - $height / 2 ) / ( $width / 3 ); my ( $re_z, $im_z, $iteration ) = (0) x 3; while (1) { my $temp = $re_z; $re_z = $re_z * $re_z - $im_z * $im_z + $re_c; $im_z = 2 * $temp * $im_z + $im_c; ++$iteration; last if $re_z * $re_z + $im_z * $im_z > 4; $iteration = 0, last if $iteration == 20; } return $iteration; } __END__

Best regards, Karl

«The Crux of the Biscuit is the Apostrophe»

Replies are listed 'Best First'.
Re: Refactoring: Better Variable Names For Better Understanding?
by BrowserUk (Patriarch) on Jun 04, 2015 at 23:13 UTC

    I'd re-work that like this:

    use constant LIMIT => 20; ## It's a constant. (Should do the same for +4) my @palette = Imager::Color->new( 0,0,0 ); ## Set the first color to b +lack avoids the need for $div ... my ( $re_z, $im_z, iterations ) = (0) x 3; while ( 1 ) { my $saved = $re_z; ## It's just saving a value for use lat +er in the calculation; $temp might also do. $re_z = $re_z * $re_z - $im_z * $im_z + $re_c; $im_z = 2 * $saved * $im_z + $im_c; ++$iterations; last if $re_z * $re_z + $im_z * $im_z > 4; ## Exit and use + the iteration count to pick a color $iterations = 0, last if $iterations == LIMIT; ## beyond t +he limit, set the iterations to 0 to pick black. } $image->setpixel( x => $x, y => $y, color => $palette[ $iterat +ions ] )

    With the rise and rise of 'Social' network sites: 'Computers are making people easier to use everyday'
    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    "Science is about questioning the status quo. Questioning authority". I'm with torvalds on this
    In the absence of evidence, opinion is indistinguishable from prejudice. Agile (and TDD) debunked

      Thank you very much for advice BrowserUK and best regards,

      Karl

      «The Crux of the Biscuit is the Apostrophe»

Re: Refactoring: Better Variable Names For Better Understanding?
by kcott (Archbishop) on Jun 04, 2015 at 23:44 UTC

    G'day karlgoethebier,

    Here's some suggestions for new names along with my rationale for choosing them. If you don't like my suggestions, that's fine; however, perhaps the rationale I've used may help you choosing ones more to your liking.

    $test is a boolean which defines whether or not to stop iterating. This could be when you've completed $iterations iterations or earlier if "$re_z * $re_z + $im_z * $im_z > 4" is TRUE. How about renaming $test to $stop_iterating.

    $div is a boolean which defines whether or not you're in the (Mandelbrot) set. specifies whether or not the pixel gets a colour. If TRUE, the pixel gets a colour from the palette; if FALSE, the pixel gets black. How about renaming $div to $in_set $colour_pixel.

    I think $old_re_z is reasonably meaningful as it stands. I might have used $last_re_z.

    Update (Wed 10 Jun 2015 15:49:34 AEST):

    Changed the paragraph starting "$div is a boolean ...". This fixes issues noted by karlgoethebier in his response below.

    -- Ken

      Thank you very much for advice kcott and best regards,

      Karl

      «The Crux of the Biscuit is the Apostrophe»

      With some delay...some more thoughts:

      "$div is a boolean which defines whether or not you're in the (Mandelbrot) set. If TRUE, the pixel gets a colour from the palette; if FALSE, the pixel gets black. How about renaming $div to $in_set"

      According to this If TRUE would mean that the pixel is in the set (colored) and If FALSE would mean that the pixel is not in the set (black), right?

      But i think it should be the other way round because the set is traditionally painted in black:

      "A point c is colored black if it belongs to the set, and white if not."

      N.B.: Threads like this are also a challenge to my skills in English.

      My best regards, Karl

      «The Crux of the Biscuit is the Apostrophe»

        "... whether or not ..."

        In this context, it refers to the two states of the boolean variable: whether (ON) or not (OFF). Perhaps "egal, ob ..."?

        "But i think it should be the other way round ..."

        See update to my original response.

        -- Ken

Re: Refactoring: Better Variable Names For Better Understanding? [SOLVED]
by marioroy (Prior) on Jun 06, 2015 at 01:02 UTC

    A parallel version for the updated code in the first post. Workers must be spawned prior to creating the image. Otherwise, Perl will crash from the Imager module not thread safe. This is also the case for the GD module.

    use strict; use warnings; use Imager; use MCE; # based on original http://www.alfrog.com/mandel.html # karlgoethebier: code refactor # marioroy: parallelization my $width = 1280; my $height = 1024; my $iterations = 20; my @palette; my $image; my $mce = MCE->new( use_threads => 0, # MCE defaults to threads on windows max_workers => 'auto', chunk_size => 8, gather => sub { my ( $x, $y, $color ); while ( ($x, $y, $color) = splice(@_, 0, 3) ) { $image->setpixel( x => $x, y => $y, color => $palette[$color] + ); } }, user_func => sub { my ( $mce, $chunk_ref, $chunk_id ) = @_; my ( $re_c, $im_c, $re_z, $im_z, $color, $temp ); my ( @set_data ); for my $x ( $chunk_ref->[0] .. $chunk_ref->[1] ) { for my $y ( 0 .. $height - 1 ) { $re_c = ( $x - 3 * $width / 4 ) / ( $width / 3 ); $im_c = ( $y - $height / 2 ) / ( $width / 3 ); $re_z = $im_z = $color = 0; while ( 1 ) { $temp = $re_z; $re_z = $re_z * $re_z - $im_z * $im_z + $re_c; $im_z = 2 * $temp * $im_z + $im_c; ++$color; last if $re_z * $re_z + $im_z * $im_z > 4; if ( $color == $iterations ) { $color = 0; last; } } push @set_data, $x, $y, $color; } } MCE->gather( @set_data ); } ); # spawn MCE workers $mce->spawn; # init image and color palette $image = Imager->new( xsize => $width, ysize => $height ); push @palette, Imager::Color->new( 0, 0, 0 ); # black for ( 1 .. $iterations ) { my ( $r, $g, $b ) = map { int rand 255 } 1 .. 3; push @palette, Imager::Color->new( $r, $g, $b ); } # compute mandelbrot $mce->process({ bounds_only => 1, sequence => [ 0, $width - 1 ] }); $mce->shutdown; # save image my $file = qq(mandelbrot.png); $image->write( file => $file );

      Results: The time needed to compute a Mandelbrot from a Linux VM configured with 4 cores at 2.6 GHz (Haswell Core i7).

      # width => 1280, height => 1024   (  1.3 million pixels )
      
        Original       10.622 secs  eval statements slow it down
        Imager          7.219 secs
        GD              3.689 secs
        MCE + Imager    3.331 secs
        MCE + GD        0.565 secs
        MCE + GD + C    0.139 secs
      
      # width => 3840, height => 3072   ( 11.8 million pixels )
      
        Original       96.129 secs  eval, ditto
        Imager         64.229 secs
        GD             33.190 secs
        MCE + Imager   29.029 secs
        MCE + GD        4.344 secs
        MCE + GD + C    0.436 secs
      
      # width => 6400, height => 5120   ( 32.8 million pixels )
      
        Original      259.215 secs  ditto
        Imager        178.817 secs
        GD             93.168 secs
        MCE + Imager   79.986 secs
        MCE + GD       12.296 secs
        MCE + GD + C    0.997 secs  <-- fast is possible :)
        MCE + GD + C    1.002 secs  <-- repeatable
        MCE + GD + C    0.989 secs  <--
      
      

      Update: Both MCE + GD and MCE + GD + Inline::C compute the upper half only.

      Update: The writer is no longer the hold up for MCE + GD and MCE + GD + Inline::C. The logic was updated to minimize GD calls by 56x.

      Update: Reduced IPC for the MCE + GD + Inline::C demonstration. Wanted to see if possible to reach below 1.0 second. Btw, the time is the unix time which includes the time to launch Perl, load modules, spawn workers, compute the Mandelbrot 6400 x 5120 (upper half only), copy/flip/copy into bottom half, write the image, and shutdown workers. It is mind-boggling.

      Thank you karlgoethebier and perlmonks. Thank you also to the original author wherever you are.

      Kind regards, Mario

        "Thank you karlgoethebier...

        Definitely too much honor for me. I need to thank you! What should i say else? Great work!

        My very best regards, Karl

        BrowserUK should also be mentioned in dispatches because he suggested at least three optimizations: omitting unnecessary variables, drawing by line and flip.

        «The Crux of the Biscuit is the Apostrophe»

      The GD module is nearly 2x faster than Imager. Not sure why. Anyway, here is the parallel version using GD. Workers are spawned prior to the image being created by the manager process.

      use strict; use warnings; use GD; use MCE; # based on original http://www.alfrog.com/mandel.html # karlgoethebier: code refactor # marioroy: parallelization my $width = 1280; my $height = 1024; my $iterations = 20; my @palette; my $image; my $mce = MCE->new( use_threads => 0, # MCE defaults to threads on Windows max_workers => 'auto', chunk_size => 8, gather => sub { my ( $x, $y, $color ); while ( ($x, $y, $color) = splice(@_, 0, 3) ) { $image->setPixel( $x, $y, $palette[ $color ] ); } }, user_func => sub { my ( $mce, $chunk_ref, $chunk_id ) = @_; my ( $re_c, $im_c, $re_z, $im_z, $color, $temp ); my ( @set_data ); for my $x ( $chunk_ref->[0] .. $chunk_ref->[1] ) { for my $y ( 0 .. $height - 1 ) { $re_c = ( $x - 3 * $width / 4 ) / ( $width / 3 ); $im_c = ( $y - $height / 2 ) / ( $width / 3 ); $re_z = $im_z = $color = 0; while ( 1 ) { $temp = $re_z; $re_z = $re_z * $re_z - $im_z * $im_z + $re_c; $im_z = 2 * $temp * $im_z + $im_c; ++$color; last if $re_z * $re_z + $im_z * $im_z > 4; if ( $color == $iterations ) { $color = 0; last; } } push @set_data, $x, $y, $color; } } MCE->gather( @set_data ); } ); # spawn MCE workers $mce->spawn; # init image and color palette $image = new GD::Image( $width, $height ); push @palette, $image->colorAllocate( 0, 0, 0 ); # black for ( 1 .. $iterations ) { my ( $r, $g, $b ) = map { int rand 255 } 1 .. 3; push @palette, $image->colorAllocate( $r, $g, $b ); } # compute mandelbrot $mce->process({ bounds_only => 1, sequence => [ 0, $width - 1 ] }); $mce->shutdown; # save image open my $fh, '>mandelbrot.png' || exit 1; binmode $fh; print $fh $image->png; close $fh;

        Update: Corrected a couple 1 off errors.

        Update: Compute upper half only. Makes a copy afterwards, flips vertically, and copies into bottom half. There is a one time delay from Inline::C compiling the C code. Remember to run again.

        Update: Draw lines when possible.

        Update: Reduced IPC.

        Finally, providing a demonstration combining MCE::Flow + GD + Inline::C.

        use strict; use warnings; use MCE::Flow Sereal => 1; use MCE::Queue Fast => 1; use GD; # based on original code at http://www.alfrog.com/mandel.html # karlgoethebier: initial code refactor, thanks perlmonks # marioroy: parallelization via MCE::Flow (2 user tasks) + Inline::C # process upper half only (reduces work for writer by 50%) # draw lines when possible (reduces work for writer by 56x) # reduced # of times to enqueue/dequeue to lower IPC # also see tips by BrowserUk at: # http://www.perlmonks.org/?node_id=1128885 use Inline 'C' => Config => CCFLAGSEX => '-O2'; use Inline 'C' => <<'END_C'; int width, height, iterations, middle; void c_init( int _width, int _height, int _iterations ) { width = _width; height = _height; iterations = _iterations; middle = _height / 2; } AV * c_mandel( int x1, int x2 ) { AV *ret = newAV(); SV *line_size; double re_c, im_c, re_z, im_z, temp; int x, y, color, last_color; for (x = x1; x <= x2; x++) { for (y = 0; y <= middle; y++) { re_c = (double) ( x - 3 * width / 4 ) / ( width / 3 ); im_c = (double) ( y - height / 2 ) / ( width / 3 ); re_z = im_z = (double) 0.0; color = 0; while ( 1 ) { temp = (double) re_z; re_z = (double) re_z * re_z - im_z * im_z + re_c; im_z = (double) 2 * temp * im_z + im_c; ++color; if ( re_z * re_z + im_z * im_z > 4.0 ) break; if ( color == iterations ) { color = 0; break; } } if ( y && color == last_color ) { // increment line size, writer draws a line when > 0 line_size = *av_fetch( ret, -1, TRUE ); sv_setiv( line_size, SvIV( line_size ) + 1 ); } else { av_push( ret, newSViv( x ) ); av_push( ret, newSViv( y ) ); av_push( ret, newSViv( color ) ); av_push( ret, newSViv( 0 ) ); last_color = color; } } } return sv_2mortal( ret ); } END_C # fasten your seat belt, enjoy the ride :) my $width = 1280 * 1; my $height = 1024 * 1; my $iterations = 20; my $Q = MCE::Queue->new(); my $num_mandels = 3; my $num_writers = 1; # must be 1 # init C and MCE c_init( $width, $height, $iterations ); MCE::Flow::init { bounds_only => 1, chunk_size => 16, max_workers => [ $num_mandels, $num_writers ], task_name => [ 'tsk_mandel', 'tsk_writer' ], user_begin => sub { my ( $mce, $task_id, $task_name ) = @_; $mce->{ret} = [] if $task_name eq 'tsk_mandel'; }, user_end => sub { my ( $mce, $task_id, $task_name ) = @_; if ( $task_name eq 'tsk_mandel' ) { $Q->enqueue( MCE->freeze( $mce->{ret} ) ) if @{ $mce->{ret} } +; $Q->enqueue( undef ); } } }; # compute mandelbrot (user tasks; sequence begin, end ) MCE::Flow::run_seq( \&mandel, \&writer, 0, $width - 1 ); MCE::Flow::finish; # for MCE providers sub mandel { my ( $mce, $chunk_ref, $chunk_id ) = @_; push @{ $mce->{ret} }, @{ c_mandel( @{ $chunk_ref } ) }; if ( @{ $mce->{ret} } > 12000 ) { $Q->enqueue( MCE->freeze( $mce->{ret} ) ); $mce->{ret} = []; } } # for MCE consumer sub writer { my ( $mce ) = @_; # init image and color palette my $image = new GD::Image( $width, $height ); my @palette = $image->colorAllocate( 0, 0, 0 ); # black for ( 1 .. $iterations ) { my ( $r, $g, $b ) = map { int rand 255 } 1 .. 3; push @palette, $image->colorAllocate( $r, $g, $b ); } # process draw requests while (1) { my $ret = $Q->dequeue; if (!defined $ret) { last unless --$num_mandels; next; } my $data = MCE->thaw( $ret ); my $size = @{ $data }; for ( my $i = 0; $i < $size; $i += 4 ) { if ( $data->[$i+3] ) { # draw line if line_size > 0 $image->line( $data->[$i], # x1 $data->[$i+1], # y1 $data->[$i], # x2 $data->[$i+1] + $data->[$i+3], # y2 $palette[ $data->[$i+2] ] # color ); } else { # otherwise, set pixel $image->setPixel( $data->[$i], # x $data->[$i+1], # y $palette[ $data->[$i+2] ] # color ); } } } # copy upper half, flip vertically, then copy into bottom half my $middle = int( $height / 2 ); my $temp = new GD::Image( $width, $middle ); $temp->copy( $image, 0, 0, 0, 0, $width, $middle ); $temp->flipVertical(); $image->copy( $temp, 0, $middle + 1, 0, 0, $width, $middle ); # save image open my $fh, '>mandelbrot.png' || return 1; binmode $fh; print $fh $image->png; close $fh; }

        Update: Corrected a couple 1 off errors.

        Update: Computes the upper half only. Makes a copy afterwards, flips vertically, and copies into bottom half.

        Update: Draw lines when possible.

        The following is constructed using MCE::Flow with 2 tasks (many providers and one writer). Spawning workers early is not necessary for this demonstration. I went with GD due to running faster than Imager. However, one can easily change to Imager inside the writer function.

        use strict; use warnings; use MCE::Flow Sereal => 1; use MCE::Queue Fast => 1; use GD; # based on original code at http://www.alfrog.com/mandel.html # karlgoethebier: initial code refactor, thanks perlmonks # marioroy: parallelization via MCE::Flow (2 user tasks) # process upper half only (reduces work for writer by 50%) # draw lines when possible (reduces work for writer by 56x) # also see tips by BrowserUk at: # http://www.perlmonks.org/?node_id=1128885 # fasten your seat belt, enjoy the ride :) my $width = 1280 * 1; my $height = 1024 * 1; my $iterations = 20; my $Q = MCE::Queue->new(); my $num_mandels = 3; my $num_writers = 1; # must be 1 # init MCE MCE::Flow::init { bounds_only => 1, chunk_size => 16, max_workers => [ $num_mandels, $num_writers ], task_name => [ 'tsk_mandel', 'tsk_writer' ], user_end => sub { my ( $mce, $task_id, $task_name ) = @_; $Q->enqueue(undef) if ( $task_name eq 'tsk_mandel' ); } }; # compute mandelbrot (user tasks; sequence begin, end ) MCE::Flow::run_seq( \&mandel, \&writer, 0, $width - 1 ); MCE::Flow::finish; # for MCE providers sub mandel { my ( $mce, $chunk_ref, $chunk_id ) = @_; my ( $re_c, $im_c, $re_z, $im_z, $color, $last_color, $temp ); my ( @ret, $middle ); $middle = int( $height / 2 ); for my $x ( $chunk_ref->[0] .. $chunk_ref->[1] ) { for my $y ( 0 .. $middle ) { $re_c = ( $x - 3 * $width / 4 ) / ( $width / 3 ); $im_c = ( $y - $height / 2 ) / ( $width / 3 ); $re_z = $im_z = $color = 0; while ( 1 ) { $temp = $re_z; $re_z = $re_z * $re_z - $im_z * $im_z + $re_c; $im_z = 2 * $temp * $im_z + $im_c; ++$color; last if $re_z * $re_z + $im_z * $im_z > 4; if ( $color == $iterations ) { $color = 0; last; } } if ( $y && $color == $last_color ) { # increment line size, writer draws a line when > 0 $ret[-1]++; } else { push @ret, $x, $y, $color, 0; $last_color = $color; } } } # freezing here to prevent double freezing/thawing $Q->enqueue( MCE->freeze( \@ret ) ); } # for MCE consumer sub writer { my ( $mce ) = @_; # init image and color palette my $image = new GD::Image( $width, $height ); my @palette = $image->colorAllocate( 0, 0, 0 ); # black for ( 1 .. $iterations ) { my ( $r, $g, $b ) = map { int rand 255 } 1 .. 3; push @palette, $image->colorAllocate( $r, $g, $b ); } # process draw requests while (1) { my $ret = $Q->dequeue; if (!defined $ret) { last unless --$num_mandels; next; } my $data = MCE->thaw( $ret ); my $size = @{ $data }; for ( my $i = 0; $i < $size; $i += 4 ) { if ( $data->[$i+3] ) { # draw line if line_size > 0 $image->line( $data->[$i], # x1 $data->[$i+1], # y1 $data->[$i], # x2 $data->[$i+1] + $data->[$i+3], # y2 $palette[ $data->[$i+2] ] # color ); } else { # otherwise, set pixel $image->setPixel( $data->[$i], # x $data->[$i+1], # y $palette[ $data->[$i+2] ] # color ); } } } # copy upper half, flip vertically, then copy into bottom half my $middle = int( $height / 2 ); my $temp = new GD::Image( $width, $middle ); $temp->copy( $image, 0, 0, 0, 0, $width, $middle ); $temp->flipVertical(); $image->copy( $temp, 0, $middle + 1, 0, 0, $width, $middle ); # save image open my $fh, '>mandelbrot.png' || return 1; binmode $fh; print $fh $image->png; close $fh; }

        The examples demonstrate using the Core MCE API and MCE::Flow.