Top Banner
Memory Un-manglement With Perl How to do what you do without getting hit in the memory. Steven Lembark Workhorse Computing
32

Memory unmanglement

Jul 16, 2015

Download

Documents

Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: Memory unmanglement

Memory Un­manglement With Perl

How to do what you dowithout getting hit in the memory.

Steven LembarkWorkhorse Computing

Page 2: Memory unmanglement

In Our Last Episode...

● We saw our hero battling the forces of ram­bloat in long­running, heavily­forked, or large­scale processes.

● Learned the golden rule: Nothing Shrinks.● Observed memory benchmarks using Devel::Peek, 

 Devel::Size, and perl -d.● peek() shows the structure & hash efficiency.● size() & total_size() show memory usage.

Page 3: Memory unmanglement

Time vs. Space

● The classic trade­off is handled in favor of time in the perl implementations.

● More efficient data structures can help both sides.● Avoiding wasted space can help avoid thrashing, heap 

management, and system call overhead.● Faster access for arrays can make them more compact 

and faster than hashes in some situations.

● Benchmarks are not only for time: include checks of size(), total_size(), and peek() to see what is really going on.

Page 4: Memory unmanglement

Nothing Ever Shrinks

● perl maintains strings and arrays as pointers to memory allocations.● Adjusting the size of a scalar with substr or a regex 

changes it start and length.● shift and pop adjust an array's initial offset and count.

● None of these will reduce the memory overhead of the 'scaffolding' perl uses to manage the data.

Page 5: Memory unmanglement

Look Deep Into Your Memory

● Devel::Peek● peek() at the structure● Shows efficiency of hashing.

● Devel::Size● size() shows memory usage of “scaffolding”.● total_size() includes contents along with skeleton.

● size() can be useful in loops for managing size of re­cycled buffers.

Page 6: Memory unmanglement

Size & Structure

● Scalars● Reference allocations for strings with offset & length.● size() of the scalar is small, total_size() can be large.

● Arrays● Allocated list of Scalars, also with offset & length.● size() reports space for list, total_size() includes contents.

● Hashes● Hash chains are an array of arrays with min. 8 chains.● size() reports space for hash chains.

Page 7: Memory unmanglement

Taming the Beast

● There are tools for managing the memory, most of which involve some sort of time/space tradeoff.● undef can help – probably less than you think.● You can manage the lifetime of variables with lexical or 

local values.● Re­cycling buffers localizes the bloat to one structure.● Adapting your code to use more effective data structures 

offers the best solution for large data.

● Here are some ideas.

Page 8: Memory unmanglement

undef() is somewhat helpful

● Marks the variable for reclamation.● Space may not be immediately reclaimed – up to perl 

whether to add heap or recycle the undef­ed variables.

● Structures are discarded, not reduced.● This can have a significant performance overhead on 

nested, re­used data structures.

● Trade­off: space for time for re­building the skeleton of discarded structures.

● Most useful for recycling single­level structures.

Page 9: Memory unmanglement

undef­ing an Array Doesn't Zero It

● For a large, nested structure this may not save the amount of space you expect.

my @a = ();$#a = 999_999;print "Size \@a:\t", size( \@a ), "\n";

undef @a;print "Size \@a:\t", size( \@a ), "\n";

Full @a:4000200Post @a: 100

● The contents are discarded & re­allocated:

Page 10: Memory unmanglement

Recycling Buffers

● Use size() to discard and re­allocate the buffer if it grows too large.

● Pre­allocate to avoid margin­of­error added by perl when the initial allocation grows.

● Decent trade­off between re­allocating a buffer frequently and having it grow without bounds.

● Avoids one record botching the entire processing cycle.

Page 11: Memory unmanglement

Scalar Buffer

● Recycle buffer, clean it up, then copy by value.● Easiest with scalars since they don't have any nested 

structure.while( $buffer = get_data ){ $buffer =~ s/^\s+//; ... push @data, $buffer;

if( size( $buffer ) > $max_buff ) { undef $buffer; $buffer = ' ' x $max_buff; }}

Page 12: Memory unmanglement

Array Buffer

● This works well for single level buffers ­­ multi­level buffers often require too much work to rebuild.my @buff = ();$#buff = $buff_count;

while( @buff = get_data ){ ... # clean up buffer $data{ $key } = [ @buff ]; # store values

if( size( \@a ) > $buff_max ) { undef @buff; $#buff = $max_buff; }}

Page 13: Memory unmanglement

Assign Arrays Single­Pass

● Say you have to store a large number of items:

my @a = @b = ();

push @a, “” for( 1 .. 1_000_000 );@b = map { “” } ( 1 .. 1_000_000 );

print 'Size of @a: ', size( \@a ), "\n";print 'Size of @b: ', size( \@b ), "\n";

Size of @a: 4194388Size of @b: 4000100

● Push ends up with a larger structure:

Page 14: Memory unmanglement

Hashes are Huge

● Incremental assignment doesn't make hashes larger: they are 8x larger than arrays in both cases.

my %a =();my %b = ();

$a{ $_ } = “” for ( 1 .. 1_000_000 );%b = map { $_ => “” } ( 1 .. 1_000_000 );

print 'Size of %a: ', size( \%a ), "\n";print 'Size of %b: ', size( \%b ), "\n";

Size of %a: 32083244 # vs. 4000100Size of %b: 32083244 # in an array!

Page 15: Memory unmanglement

Two Ways of Storing Nothing

● There are two common ways of storing nothing in the values of  a hash:● Assign an empty list:   $hash{ $key } = ();

● Assign an empty string: $hash{ $key } = “”;

● Question:

Which would take less space: empty list or empty string?

Page 16: Memory unmanglement

TMTOWTDN

my %a =();my %b = ();

$a{ $_ } = () for( 'aaa' .. 'zzz' );$b{ $_ } = '' for( 'aaa' .. 'zzz' );

print "Size of %a:\t", size( \%a ), "\n";print "Size of %b:\t", size( \%b ), "\n";

Size of %a: 570516 # same size for “” & ()?Size of %b: 570516

● size() gives the same result for both values. Why?

Page 17: Memory unmanglement

TMTOWTDN

my %a =();my %b = ();

$a{ $_ } = () for( 'aaa' .. 'zzz' );$b{ $_ } = '' for( 'aaa' .. 'zzz' );

print "Size of %a:\t", size( \%a ), "\n";print "Size of %b:\t", size( \%b ), "\n";

print "Total in %a:\t", total_size( \%a ), "\n";print "Total in %b:\t", total_size( \%b ), "\n";

Size of %a: 570516 # size() doesn't alwaysSize of %b: 570516 # matter!

Total in %a: 851732Total in %b: 1203252

● total_size() benchmarks the values:

Page 18: Memory unmanglement

Replace Hashes With Arrays

● The smart­match operator (“~~”) is fast.● Pushing onto an array:

$a ~~ @uniq or push @uniq, $a

uses about 1/8 the space of assigning hash keys:$uniq{ $a } = ();

...

keys %uniq

● The extra space used by array growth in push is dwarfed by the savings of an array over a hash.

● sort @uniq is much faster than sort keys %uniq.

Page 19: Memory unmanglement

Example: Taxonomy Trees

● The NCBI Taxonomy is delivered with each entry having a full tree.

● These must be reduced to a single tree for data entry and validation.

● There are several ways to do this...

Page 20: Memory unmanglement

Worst Solution: Parent tree.

● Since the tree is often used from the bottom up, some people store it as a child:parent relationship:

$parentz{ $child_id } = $parent_id;

● Unfortunately, this allocates a full hash table for each 1:1 relationship between a child and parent.

Page 21: Memory unmanglement

Another Bad Solution: Child Tree

● Another alternative is storing the children in a hash for each parent:

$childz{ $parent_id }{ $child_id } = ();

$childz{ '' } = [ $root_id ];

● This works via depth­first search to generate the trees and has space to store the tree­depth.

● Hashes are bulky and slow for storing a single­level structure like this.

Page 22: Memory unmanglement

Another Solution: Single­Level Hash

● One oft­forgotten bit of Perly lore in the age of references: multi­part hash keys.

$childz{ $parent_id, $child_id } = $depth;

$childz{ “” } = [ $root_id ];

● Trades wasted space in thousands of anon hashes for split /$;/o, $key and grep's.

● Usable for moderate trees.● Obviously painful for really large trees.

Page 23: Memory unmanglement

Q: Why Nest Hashes?● Hashes are nice for the top­level lookup, but why 

nest them?

● Arrays save about 85% of the overhead below the top level.

● Any wasted space from the arrays growing via push is more than saved by avoiding hashes.

● The arrays only need to be sorted once if the tree is used multiple times.

my $c = $childz{ $parent_id } ||=[];

$new_id ~~ $cor push @{ $c{ $parent_id } }, $new_id;

Page 24: Memory unmanglement

Nested Lists

● List::Util has first() which saves grep­ing entire lists.● A key and payload on an array can be handled 

quickly.first { $_->[0] eq $key } @data;

● For shorter lists this saves space and can be faster than a hash.

● This is best for numerics, which don't have to be converted to text in order to be hashed: $_->[0] == $value is the least amount of work to compare integers.

Page 25: Memory unmanglement

Manage Lifespans

● Lexical variables are an obvious place.● Local values are another.

● Saves re­allocating a set of values within tight loops in the called code.

● Local hash keys are a good way to manage storage in re­used hashes handled with recursion.

● Use delete to remove hash keys in multi­level structures instead of assigning an empty list or “”.● This preserves the skeleton for re­cycling.● Saves storing the keys.

Page 26: Memory unmanglement

Use Simpler Objects

● If you're using inside­out objects, why bless a hash?● Users aren't supposed to diddle around inside your 

objects anyway.

● The only thing you care about is the address.● Bless something smaller:

my $obj = bless \(my $a), $package;

Page 27: Memory unmanglement

Use Linked Lists for Queues

● Automatically frees discarded nodes without having to modify the entire list. 

● Based on an array they don't use much extra data:$node = [ $ref_to_next, @node_data ];

● Walking the list is simple enough:( $node, my @data ) = @$node;

● So is removing a node:$node->[0] = $node->[0][0];

● These are quite convenient for threading.

Page 28: Memory unmanglement

Use Hashes for Sparse Arrays

● OK, Time to stop beating up on hashes.● They beat out arrays for sparse lists.● Even list of integers.

● Say a collection of DNA runs from 15 to 10_000 bases, filling about 10% of the actual values.

● You could store it as:$dnaz[ $length ] = [ qw( dna dna dna ) ];

● But this is probably better stored in a hash:$dnaz{ $length } = [ qw( dna dna dna ) ];

Page 29: Memory unmanglement

Accessing Hash Keys: Integer Slices

● Numeric sequences work fine as hash keys.● Say you want to find all of the sequences within 

+/ 10% of the current length:‑

my $min = 0.9 * $length;my $max = 1.1 * $length;my @found = grep{ $_ } @dnaz{ ( $min .. $max ) };

● For non­trivial, sparse lists this saves scaffolding by only storing the structure necessary.

● This doesn't change the data storage, just the overhead for accessing it by length.

Page 30: Memory unmanglement

Store Upper­triangular Comparisons

● Saves more than half the space.● Accessor can look for $i > $j ? [$i][$j] : [$j][$i] and 

get the same results.● Requires designing symmetric comparison 

algorithms (values can be returned as­is or just negated).

● Also saves about half the processing time to only generate a single comparison for each pair.

● Requires access to the algorithm.

Page 31: Memory unmanglement

Example: DNA Analysis

● Our W­curve analysis is used to compare large groups of DNA to one another.

● The original algorithm compared the curves until the first one was exhausted.

● Changing that to use the longer sequence in all cases saved us over half the comparison time.

Page 32: Memory unmanglement

Summary

● Devel::Size can be useful in your code.● Managing the lifespan of values helps.● Using efficient structures helps even more.

● Use arrays instead of hash structures where they make sense.

● Bless smaller structures: scalars, regexen, globs make perfectly good objects and take less space than hashes.

● Use XS or Inline where necessary.● And, yes, size() still matters.