-- Benchmark name declarations local 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", } local benchmarks_memory = { "facebook", "reddit", "flickr", "theverge", "nimlang" } local benchmarks_memory_warning = { "reddit_memory_warning --runs 0", "flickr_memory_warning --runs 0", "theverge_memory_warning --runs 0", } -- Allocator names mapped to the required environemnt variable setting. local allocator_env_vars = { -- Note that bmalloc requires MallocNanoZone=1 ["bmalloc"] = "MallocNanoZone=1", ["SystemMalloc"] = "MallocNanoZone=0", ["NanoMallocV1"] = "MallocNanoZone=V1", ["NanoMallocV2"] = "MallocNanoZone=V2", } -- Constants local executionTimeName = "executionTime" local peakMemoryName = "peakMemory" local memoryAtEndName = "memoryAtEnd" local arithmeticMean = "a" local geometricMean = "g" local harmonicMean = "h" local arithmeticMeanName = "" local geometricMeanName = "" local harmonicMeanName = "" local displayMeanNames = { [arithmeticMean] = arithmeticMeanName, [geometricMean] = geometricMeanName, [harmonicMean] = harmonicMeanName, } -- Shared state local benchmarks = nil local heap = 0 local quiet = false local oneRun = false local mallocBench = nil local jsonPath = nil local fileSep = package.config:sub(1,1) local dirPattern = "(.*)" .. fileSep .. "[^" ..fileSep .. "]*" local usrLocalLibMbmalloc = "/usr/local/lib/libmbmalloc.dylib" -- Argument parsing local usage = [[run-malloc-benchmarks [options] /path/to/MallocBench Name:/path/to/libmbmalloc.dylib [ Name:/path/to/libmbmalloc.dylib ... ] Runs a suite of memory allocation and access benchmarks. is a symbolic name followed by a path to libmbmalloc.dylib. Specify \"SystemMalloc\" to test the built-in libc malloc using the system allocators (no NanoMalloc). Specify \"NanoMalloc\" to test the built-in libc malloc using the default NanoMalloc allocator. Specify \"NanoMallocV1\" to test the built-in libc malloc using the NanoMalloc V1 allocator. Specify \"NanoMallocV2\" to test the built-in libc malloc using the NanoMalloc V2 allocator. Example usage: run-malloc-benchmarks /BUILD/MallocBench SystemMalloc:/BUILD/libmbmalloc.dylib NanoMalloc:/BUILD/libmbmalloc.dylib run-malloc-benchmarks /BUILD/MallocBench FastMalloc:/BUILD/FastMalloc/libmbmalloc.dylib run-malloc-benchmarks --benchmark churn SystemMalloc:/BUILD/libmbmalloc.dylib FastMalloc:/BUILD/FastMalloc/libmbmalloc.dylib Options: --one_run Run the test only once and without cache warmup. --json Write the results to "filename" as JSON. --quiet Do not write the results to standard output. --benchmark Select a single benchmark to run instead of the full suite. --heap Set a baseline heap size for bmalloc. -h, --help Show this help message and exit. ]] local function dirname(path) local _, _, dir = path:find(dirPattern) return dir end local function parseArguments(cmdline) local function toHeapSize(valueStr) local result = tonumber(valueStr) return result and result >= 0 and result == math.floor(result) and result or nil end local function contains_value(table, value) for _, v in pairs(table) do if v == value then return true end end return false end local argparse = require "argparse" local parser = argparse() parser:usage(usage) parser:help(usage) parser:option("--json"):args(1) parser:option("--quiet"):args(0) parser:option("--heap"):args(1):default(0):convert(toHeapSize) parser:option("--benchmark"):args(1) parser:option("--memory"):args(0) parser:option("--memory_warning"):args(0) parser:option("--one_run"):args(0) parser:argument("malloc_bench", "Path to MallocBench executable"):args(1) parser:argument("dylibs", "One or more mbmalloc dylibs"):args("1+") local options = parser:parse(cmdline) local lfs = require "lfs" local pwd = lfs.currentdir() local fileSep = package.config:sub(1,1) -- Get the JSON output path, if there is one. jsonPath = options["json"] if jsonPath then jsonPath = (jsonPath:sub(1, 1) == fileSep and jsonPath) or pwd .. fileSep .. jsonPath end -- Determine whether to run only once if options["one_run"] then oneRun = true end -- Suppress output to stdout, if requested. if options["quiet"] then quiet = true end -- Set the heap value, defaulted to 0 (so always present) heap = options["heap"] -- Set the benchmarks to be run, defaulting to all of them. local benchName = options["benchmark"] if benchName then assert(contains_value(benchmarks_all, benchName) or contains_value(benchmarks_memory, benchName), "Invalid benchmark name: " .. benchName) benchmarks = { benchName } end if options["memory"] then if benchmarks then error("Only one of --benchmark, --memory, --memory_warning can be used") end benchmarks = benchmarks_memory end if options["memory_warning"] then if benchmarks then error("Only one of --benchmark, --memory, --memory_warning can be used") end benchmarks = benchmarks_memory_warning end if not benchmarks then benchmarks = benchmarks_all end -- Ensure that the malloc_bench executable exists mallocBench = options["malloc_bench"] mallocBench = (mallocBench:sub(1, 1) == fileSep and mallocBench) or pwd .. fileSep .. mallocBench assert(lfs.attributes(mallocBench), string.format("Invalid malloc_bench reference: %s", mallocBench)) -- Build the list of dylibs. The parser ensures that there is at least one. -- Each argument is of the form name:path where path refers to an mbmalloc dylib -- and name is the name of the allocator to use. local dylibs = {} local dylibPaths = {} local dylibArgs = options["dylibs"] for _, v in pairs(dylibArgs) do local s, _, v1, v2 = string.find(v, "^(%w+):(.*)") assert(s, string.format("Invalid dylib selector: %s", v)) assert(allocator_env_vars[v1], string.format("Invalid allocator name: %s", v1)) local full_path = (v2:sub(1, 1) == fileSep and v2) or pwd .. fileSep .. v2 assert(lfs.attributes(full_path), string.format("Invalid dylib reference: %s", full_path)) table.insert(dylibs, v1) table.insert(dylibPaths, full_path) end return dylibs, dylibPaths end -- Benchmark execution local function addResult(results, benchmark, dylib, executionTime, peakMemory, memoryAtEnd) results[executionTimeName][dylib][benchmark] = executionTime results[peakMemoryName][dylib][benchmark] = peakMemory results[memoryAtEndName][dylib][benchmark] = memoryAtEnd end local function runBenchmarks(dylibs, dylibPaths) -- Initialize the results for each metric and dylib to empty. local results = {} results[executionTimeName] = {} results[peakMemoryName] = {} results[memoryAtEndName] = {} for _, dylib in pairs(dylibs) do results[executionTimeName][dylib] = {} results[peakMemoryName][dylib] = {} results[memoryAtEndName][dylib] = {} end -- Run each benchmark for each dylib and add the result to "results". local mallocBenchDir = dirname(mallocBench) for _, benchmark in pairs(benchmarks) do for i, dylib in ipairs(dylibs) do local dylibPath = dylibPaths[i] local dylibDir = dirname(dylibPath) local envVars = "DYLD_LIBRARY_PATH=" .. dylibDir .. " " .. allocator_env_vars[dylib] envVars = envVars .. " DYLD_PRINT_LIBRARIES=1 " local cmd = "cd '" .. mallocBenchDir .. "'; " .. envVars .. " '" .. mallocBench .. "' " .. "--benchmark " .. benchmark .. " --heap " .. heap if oneRun then cmd = cmd .. " --runs 1 --no-warm" end io.write(string.format("\nCMD is %s\n", cmd)); io.write(string.format("\rRUNNING %s: %s... ", benchmark, dylib)) local f = assert(io.popen(cmd, "r")) local str = assert(f:read("*a")) f:close() --[[ Typical result in "str": Running churn [ not parallel ] [ don't use-thread-id ] [ heap: 0MB ] [ runs: 8 ]... Time: 69.2164ms Peak Memory: 5444kB Memory at End: 876kB Capture the Time, Peak Memory and Memory at End values as the result. ]] local resultLines = {} for line in str:gmatch("([^\r\n]+)") do table.insert(resultLines, line) end assert(#resultLines == 4, string.format("Unexpected benchmark result: %s\n", str)) local time = tonumber(resultLines[2]:match("([%d]+)")) local peakMemory = tonumber(resultLines[3]:match("([%d]+)")) local memoryAtEnd = tonumber(resultLines[4]:match("([%d]+)")) addResult(results, benchmark, dylib, time, peakMemory, memoryAtEnd) end end io.write("\r \n") return results end -- Results output local function computeArithmeticMean(values) local sum = 0.0 local count = 0 for _, value in pairs(values) do sum = sum + value count = count + 1 end return math.modf(sum/count) end local function computeGeometricMean(values) local product = 1.0 local count = 0 for _, value in pairs(values) do product = product * value count = count + 1 end return math.modf(product ^ (1/count)) end local function computeHarmonicMean(values) local sum = 0.0 local count = 0 for _, value in pairs(values) do sum = sum + 1/value count = count + 1 end return math.modf(count/sum) end local function lowerIsBetter(a, b, better, worse) if a == 0 or b == 0 or b == a then return "" end if b < a then return string.format("^ %.2fx %s", a/b, better) end return string.format("! %.2fx %s", b/a, worse) end local function lowerIsFaster(a, b) return lowerIsBetter(a, b, "faster", "slower") end local function lowerIsSmaller(a, b) return lowerIsBetter(a, b, "smaller", "bigger") end local function prettify(number, suffix) local left, num, right = string.match(number,'^([^%d]*%d)(%d*)(.-)$') return left .. num:reverse():gsub('(%d%d%d)','%1,'):reverse() .. right .. suffix end local function ljustify(str, width) -- string.format does not understand "%*.*s". local fmt = string.format("%%%d.%ds", -width, width) return string.format(fmt, str) end local function rjustify(str, width) -- string.format does not understand "%*.*s". local fmt = string.format("%%%d.%ds", width, width) return string.format(fmt, str) end local function calculateMeans(dylibs, results) local means = { [executionTimeName] = {}, [peakMemoryName] = {}, [memoryAtEndName] = {} } for metric in pairs(means) do for _, dylib in ipairs(dylibs) do means[metric][dylib] = {} means[metric][dylib][geometricMean] = results[metric][dylib] and computeGeometricMean(results[metric][dylib]) or 0 means[metric][dylib][arithmeticMean] = results[metric][dylib] and computeArithmeticMean(results[metric][dylib]) or 0 means[metric][dylib][harmonicMean] = results[metric][dylib] and computeHarmonicMean(results[metric][dylib]) or 0 end end return means end local function printResults(dylibs, results, means) local leadingPad = " " local leadingPadLength = #leadingPad local width = #arithmeticMeanName for _, name in pairs(benchmarks) do local w = #name if w > width then width = w end end width = width + leadingPadLength local function printHeader(dylibs) headers = rjustify("", width) .. rjustify(dylibs[1], width) if #dylibs > 1 then for i = 2, #dylibs do headers = headers .. rjustify(dylibs[i], width) .. rjustify("Δ", width) end end print(headers) end local function printMean(mean, results, dylibs, means, compareFunction, units) -- Display the mean for the first dylib local meanName = displayMeanNames[mean] local baseDylib = dylibs[1] local str = leadingPad .. ljustify(meanName, width - leadingPadLength) .. rjustify(prettify(means[baseDylib][mean], units), width) -- For each subsequent dylib, show the mean and the ratio wrt the first dylib. for index = 2, #dylibs do local dylib = dylibs[index] str = str .. rjustify(prettify(means[dylib][mean], units), width) .. rjustify(compareFunction(means[baseDylib][mean], means[dylib][mean]), width) end print(str) end local function printMetric(title, results, means, metricName, compareFunction, units) print(title .. ":") -- Benchmark results. One row for each benchmark, one column for the first dylib -- followed by the result and comparison for the other dylibs. for _, benchmark in pairs(benchmarks) do local dylib = dylibs[1] local measurements = { results[dylib] and results[dylib][benchmark] and results[dylib][benchmark] or 0 } local str = leadingPad .. ljustify(benchmark, width - leadingPadLength) .. rjustify(prettify(measurements[1], units), width) for index = 2, #dylibs do dylib = dylibs[index] measurements[index] = results[dylib] and results[dylib][benchmark] and results[dylib][benchmark] or 0 str = str .. rjustify(prettify(measurements[index], units), width) .. rjustify(compareFunction(measurements[1], measurements[index]), width) end print(str) end -- Means print("") printMean(geometricMean, results, dylibs, means, compareFunction, units) printMean(arithmeticMean, results, dylibs, means, compareFunction, units) printMean(harmonicMean, results, dylibs, means, compareFunction, units) print("") end printHeader(dylibs) printMetric("Execution Time", results[executionTimeName], means[executionTimeName], executionTimeName, lowerIsFaster, "ms") printMetric("Peak Memory", results[peakMemoryName], means[peakMemoryName], peakMemoryName, lowerIsSmaller, "kB") printMetric("Memory at End", results[memoryAtEndName], means[memoryAtEndName], memoryAtEndName, lowerIsSmaller, "kB") end local function writeJSON(jsonPath, dylibs, results, means) local cjson = require 'cjson' local fullResults = { ["results"] = results, ["means"] = means } local jsonText = cjson.encode(fullResults) local f = assert(io.open(jsonPath, "w"), "Failed to open JSON file " .. jsonPath) f:write(jsonText) f:close() if not quiet then print("JSON results written to " .. jsonPath) end end -- Execution begins here local dylibs, dylibPaths = parseArguments(arg) local results = runBenchmarks(dylibs, dylibPaths) local means = calculateMeans(dylibs, results) if not quiet then printResults(dylibs, results, means) end if jsonPath then writeJSON(jsonPath, dylibs, results, means) end