#!/usr/bin/env ruby # coding: utf-8 require 'getoptlong' require 'pathname' $benchmarks_all = [ # Single-threaded benchmarks. "churn", "list_allocate", "tree_allocate", "tree_churn", "fragment", "fragment_iterate", "medium", "big", # Benchmarks based on browser recordings. "facebook", "reddit", "flickr", "theverge", "nimlang", # Multi-threaded benchmark variants. "message_one", "message_many", "churn --parallel", "list_allocate --parallel", "tree_allocate --parallel", "tree_churn --parallel", "fragment --parallel", "fragment_iterate --parallel", # These tests often crash TCMalloc: . "medium --parallel", "big --parallel", # Enable these tests to test memory footprint. The way they run is not # really compatible with throughput testing. # "reddit_memory_warning --runs 0", # "flickr_memory_warning --runs 0", # "theverge_memory_warning --runs 0", # Enable this test to test shrinking back down from a large heap while a process remains active. # The way it runs is not really compatible with throughput testing. # "balloon" "facebook --parallel", "reddit --parallel", "flickr --parallel", "theverge --parallel", # "nimlang --use-thread-id", ] $benchmarks_memory = [ "facebook", "reddit", "flickr", "theverge", "nimlang" ] $benchmarks_memory_warning = [ "reddit_memory_warning --runs 0", "flickr_memory_warning --runs 0", "theverge_memory_warning --runs 0", ] $benchmarks = $benchmarks_all $heap = 0 def usage puts "run-malloc-benchmarks [options] /path/to/MallocBench Name:/path/to/libmbmalloc.dylib [ Name:/path/to/libmbmalloc.dylib ]" puts puts " Runs a suite of memory allocation and access benchmarks." puts puts " is a symbolic name followed by a path to libmbmalloc.dylib." puts puts " Specify \"SystemMalloc\" to test the built-in libc malloc." puts " Specify \"NanoMalloc\" to test the built-in libc malloc using the NanoMalloc zone." puts puts " Example usage:" puts puts " run-malloc-benchmarks /BUILD/MallocBench SystemMalloc:/BUILD/libmbmalloc.dylib NanoMalloc:/BUILD/libmbmalloc.dylib" puts " run-malloc-benchmarks /BUILD/MallocBench FastMalloc:/BUILD/FastMalloc/libmbmalloc.dylib" puts " run-malloc-benchmarks --benchmark churn SystemMalloc:/BUILD/libmbmalloc.dylib FastMalloc:/BUILD/FastMalloc/libmbmalloc.dylib" puts puts "Options:" puts puts " --benchmark Select a single benchmark to run instead of the full suite." puts " --heap Set a baseline heap size." puts end class Dylib attr_reader :name attr_reader :path def initialize(name, path) @name = name @path = path end end class Results attr_reader :executionTime attr_reader :peakMemory attr_reader :memoryAtEnd def initialize(executionTime, peakMemory, memoryAtEnd) @executionTime = executionTime @peakMemory = peakMemory @memoryAtEnd = memoryAtEnd end end class Stat attr_reader :benchmark attr_reader :result def initialize(benchmark, result) @benchmark = benchmark @result = result[/\d+/].to_i end end class TimeStat < Stat def to_s @result + "ms" end end class MemoryStat < Stat def to_s @result + "kB" end end class PeakMemoryStat < Stat def to_s @result + "kB" end end def lpad(str, chars) if str.length > chars str else "%#{chars}s"%(str) end end def rpad(str, chars) while str.length < chars str += " " end str end def computeArithmeticMean(array) sum = 0.0 array.each { | value | sum += value } (sum / array.length) end def computeGeometricMean(array) mult = 1.0 array.each { | value | mult *= value ? value : 1.0 } (mult ** (1.0 / array.length)) end def computeHarmonicMean(array) 1.0 / computeArithmeticMean(array.collect{ | value | 1.0 / value }) end def lowerIsBetter(a, b, better, worse) if b < a return "^ " + (a.to_f / b.to_f).round(2).to_s + "x " + better end if b == a return "" end "! " + (b.to_f / a.to_f).round(2).to_s + "x " + worse end def lowerIsFaster(a, b) lowerIsBetter(a, b, "faster", "slower") end def lowerIsSmaller(a, b) lowerIsBetter(a, b, "smaller", "bigger") end def numberWithDelimiter(number) number.to_s.reverse.gsub(/...(?=.)/,'\&,').reverse end def prettify(number, suffix) numberWithDelimiter(number) + suffix end def parseOptions GetoptLong.new( ['--benchmark', GetoptLong::REQUIRED_ARGUMENT], ['--memory', GetoptLong::NO_ARGUMENT], ['--memory_warning', GetoptLong::NO_ARGUMENT], ['--heap', GetoptLong::REQUIRED_ARGUMENT], ['--help', GetoptLong::NO_ARGUMENT], ).each { | opt, arg | case opt when '--benchmark' $benchmarks = [ arg ] when '--memory' $benchmarks = $benchmarks_memory when '--memory_warning' $benchmarks = $benchmarks_memory_warning when '--heap' $heap = arg when '--help' usage exit 1 else raise "bad option: #{opt}" end } if ARGV.length < 1 puts "Error: No MallocBench specified." exit 1 end if ARGV.length < 2 puts "Error: No dylib specified." exit 1 end $mallocBench = File.absolute_path(ARGV.shift) if !File.exists?($mallocBench) puts "File not found: #{$mallocBench}." exit 1 end $buildDir = Pathname.new($mallocBench).dirname dylibs = [] ARGV.each { | arg | name, path = arg.split(":") if !name || name.length < 1 || !path || path.length < 1 puts "Invalid : '#{arg}'." exit 1 end dylib = Dylib.new(name, File.expand_path(path)) if !File.exists?(dylib.path) puts "File not found: #{dylib.path}." exit 1 end dylibs.push(dylib) } dylibs end def runBenchmarks(dylibs) executionTime = [] peakMemory = [] memoryAtEnd = [] $benchmarks.each { | benchmark | executionTime.push([]) peakMemory.push([]) memoryAtEnd.push([]) dylibs.each { | dylib | $stderr.print "\rRUNNING #{dylib.name}: #{benchmark}... " env = "DYLD_LIBRARY_PATH='#{Pathname.new(dylib.path).dirname}' " env += "LD_LIBRARY_PATH='#{Pathname.new(dylib.path).dirname}' " if dylib.name == "NanoMalloc" env += "MallocNanoZone=1 " elsif dylib.name == "SystemMalloc" env += "MallocNanoZone=0 " end input = "cd '#{$buildDir}'; #{env} '#{$mallocBench}' --benchmark #{benchmark} --heap #{$heap}}" output =`#{input}` splitOutput = output.split("\n") executionTime[-1].push(TimeStat.new(benchmark, splitOutput[1])) peakMemory[-1].push(PeakMemoryStat.new(benchmark, splitOutput.length > 3 ? splitOutput[2] : "0")) memoryAtEnd[-1].push(MemoryStat.new(benchmark, splitOutput.length > 2 ? splitOutput[3] : "0")) } } $stderr.print "\r \n" Results.new(executionTime, peakMemory, memoryAtEnd) end def printResults(dylibs, results) def printHeader(dylibs, fieldSize) print print lpad("", fieldSize) print lpad(dylibs[0].name, fieldSize) if dylibs.length > 1 print lpad(dylibs[1].name, fieldSize) print lpad("Δ", fieldSize) end print "\n" end def printMetric(name, results, compareFunction, suffix, fieldSize) def printMean(name, results, meanFunction, compareFunction, suffix, fieldSize) means = [] means.push(meanFunction.call(results.collect { | stats | stats[0].result })) print rpad(" " + name, fieldSize) print lpad("#{prettify(means[0].round, suffix)}", fieldSize) if results[0][1] means.push(meanFunction.call(results.collect { | stats | stats[1].result })) print lpad("#{prettify(means[1].round, suffix)}", fieldSize) print lpad(compareFunction.call(means[0], means[1]), fieldSize) end print "\n" end if results[0][0].result == 0 return end print name + ":\n" results.each { | stats | print rpad(" " + stats[0].benchmark, fieldSize) print lpad("#{prettify(stats[0].result, suffix)}", fieldSize) if stats[1] print lpad("#{prettify(stats[1].result, suffix)}", fieldSize) print lpad(compareFunction.call(stats[0].result, stats[1].result), fieldSize) end print "\n" } print "\n" printMean("", results, method(:computeGeometricMean), compareFunction, suffix, fieldSize) printMean("", results, method(:computeArithmeticMean), compareFunction, suffix, fieldSize) printMean("", results, method(:computeHarmonicMean), compareFunction, suffix, fieldSize) print "\n" end fieldSize = ($benchmarks + [""]).collect { | benchmark | benchmark.size }.max + 4 printHeader(dylibs, fieldSize) printMetric("Execution Time", results.executionTime, method(:lowerIsFaster), "ms", fieldSize) printMetric("Peak Memory", results.peakMemory, method(:lowerIsSmaller), "kB", fieldSize) printMetric("Memory at End", results.memoryAtEnd, method(:lowerIsSmaller), "kB", fieldSize) end def main begin dylibs = parseOptions() results = runBenchmarks(dylibs) printResults(dylibs, results) rescue => exception puts puts puts exception puts exception.backtrace puts end end main()