|
#!/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: <rdar://problem/13657137>. |
|
"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 " <Name:/path/to/libmbmalloc.dylib> 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 <benchmark> Select a single benchmark to run instead of the full suite." |
|
puts " --heap <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 <Name:/path/to/dylib>: '#{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("<geometric mean>", results, method(:computeGeometricMean), compareFunction, suffix, fieldSize) |
|
printMean("<arithmetic mean>", results, method(:computeArithmeticMean), compareFunction, suffix, fieldSize) |
|
printMean("<harmonic mean>", results, method(:computeHarmonicMean), compareFunction, suffix, fieldSize) |
|
|
|
print "\n" |
|
end |
|
|
|
fieldSize = ($benchmarks + ["<arithmetic mean>"]).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() |