Lets assume we want to write and run a simple microbenchmark which tests the map
method on the Scala Range
class.
This section shows the basics of how to do this.
Preparatory steps
ScalaMeter requires at least JRE 7 update 4 and Scala 2.10 to be run.
-
Make sure you have at least JRE 7 update 4 installed on your machine.
Download the latest version or update an existing one. -
Make sure you have at least Scala 2.10 installed on your machine.
Download and install Scala 2.10 if you don’t have a newer version. -
Go to the download section and download the latest release of ScalaMeter.
-
Create a new project and a new file named
RangeMicrobenchmark.scala
in your editor.
Implementing the microbenchmark
Start with the following import
statement:
import org.scalameter.api._
This gives us access to most of the ScalaMeter API. Alternatively, we can import different parts of ScalaMeter selectively, but this will do for now.
A ScalaMeter represents performance tests with the PerformanceTest
abstract class – to implement a
performance test, we have to extend this class.
A performance test can either be a singleton class
or a object
.
The only difference from ScalaMeter’s point of view is that object
performance tests will have
a main
method, hence being runnable applications.
For that reason, we choose the latter:
object RangeBenchmark
extends PerformanceTest.Quickbenchmark {
// multiple tests can be specified here
}
The PerformanceTest
abstract class is a highly configurable test template which allows more than
we need right now.
Instead of inheriting it directly, we inherit a predefined class called PerformanceTest.Quickbenchmark
which is a performance test configured to simply run the tests and output them in the terminal.
Most benchmarks need input data that they are executed on.
To define input data in a clean and composable manner ScalaMeter supports data generators
represented by the Gen
interface.
These generators are similar to the ones in frameworks like ScalaCheck
in that they are composable with for
-comprehensions and that they can generate multiple values.
However, ScalaMeter generators do not generate random or arbitrary values – the values they produce
are always the same, ordered and well-defined.
ScalaCheck generators can be roughly divided into 2 groups – basic and composed generators.
There exist a number of basic generators already defined for you.
One of them is called Gen.range
, and it generates integers in a specified range.
val sizes: Gen[Int] = Gen.range("size")(300000, 1500000, 300000)
This creates a generator which generates integers the range from 300000
to 1500000
in
steps of 300000
.
A basic generator must always be given a name – we call our basic generator size
, because
it will produce different sizes for our ranges.
Our microbenchmark will not be taking sizes as inputs – instead, it will take different ranges.
For each of the different sizes generated by the above defined generator, we need one range.
We can express this elegantly using a for
-comprehension:
val ranges: Gen[Range] = for {
size <- sizes
} yield 0 until size
This for
-comprehension says: For every size
given by the sizes
generator yield a range from 0
to size
.
It produces a new generator ranges
of type Gen[Range]
.
The new generator is a composed generator, because it has been obtain through a for
-comprehension.
We’re now done with defining input data for the benchmark, and we move on to defining the actual
code that the benchmark is supposed to evaluate.
ScalaMeter defines a custom DSL for writing tests.
The first important statement we need to know is performance of
:
performance of "Range" in {
// nested tests
}
This statement has the effect that all the tests nested in the block behind in
get a prefix Range
in their name.
You can nest performance of
blocks arbitrarily deep to divide your tests in groups and achieve the
desired hierarchy.
The related statement measure method
behaves in exactly the same way – the only difference is its
name, so you will usually write this one immediately surrounding your test:
performance of "Range" in {
measure method "map" in {
// we will write the actual test body here
}
}
In order to write the actual test, we have to tell ScalaMeter which data inputs to use.
This is done with the using
statement, which takes a generator and the snippet invoking
a some code on a range r
:
performance of "Range" in {
measure method "map" in {
using(ranges) in {
r => r.map(_ + 1)
}
}
}
And that’s it - we’ve defined a test group Range.map
consisting of a single test where the elements
of a range are map
ped so that each element is incremented by one.
For the sake of completeness, here is the complete runnable test:
import org.scalameter.api._
object RangeBenchmark
extends PerformanceTest.Quickbenchmark {
val sizes = Gen.range("size")(300000, 1500000, 300000)
val ranges = for {
size <- sizes
} yield 0 until size
performance of "Range" in {
measure method "map" in {
using(ranges) in {
r => r.map(_ + 1)
}
}
}
}
Running the benchmark
Now that we have the benchmark, it’s time to run it.
First, compile it with scalac
:
$ scalac -cp scalameter_2.10-0.1.jar RangeBenchmark.scala
Then run it:
$ scala -cp scalameter_2.10-0.1.jar:. RangeBenchmark
Alternatively, you can use SBT build tool, which is much simpler and the preferred way to run ScalaMeter tests in larger projects. A huge benefit of doing so is that you don’t have to manually pick the correct Scala version and the ScalaMeter artifact - SBT does this for you automatically. Also, with SBT you can run the tests directly from the SBT shell. See the section SBT integration for details.
After running the test, you should get an output similar to the following one:
::Benchmark Range.map::
jvm-name: Java HotSpot(TM) 64-Bit Server VM
jvm-vendor: Oracle Corporation
jvm-version: 23.0-b16
os-arch: amd64
os-name: Mac OS X
Parameters(size -> 300000): 2.0
Parameters(size -> 600000): 4.0
Parameters(size -> 900000): 7.0
Parameters(size -> 1200000): 16.0
Parameters(size -> 1500000): 30.0
The PerformanceTest.Quickbenchmark
class uses a simple terminal reporter, so
all the results of the test are just printed to the standard output.
The results are in milliseconds.
We can see that the reporter outputs some machine-specific data, followed by the
results for each of the input parameters.
Note
Your mileage may vary! When writing these guidelines, the tests were taken on a 4-core 3.4 GHz i7 iMac, Mac OS X 10.7.5, JRE 7 update 9 and Scala 2.10-RC2. With a different configuration, particularly with different hardware, you might get entirely different running times.
A PerformanceTest.Quickbenchmark
is already configured to warm up the JVM and do several tests
for each input size.
It takes the smallest time observed for each input size.
Statistically, a mean running time gives much more insight into performance characteristics, but
we will see how to obtain it later.
And that’s it – you just wrote your first ScalaMeter microbenchmark. Next, you will see how to configure test execution and test reporting.