Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Here's a link to a page about Java performance coding - avoiding unnecessary allocation for Java code. The same principles apply to Scala code: http://blog.takipi.com/5-coding-hacks-to-reduce-gc-overhead/

Avoid Unnecessary Allocation

Many things in Scala cause allocation of objects on the heap. This involves quite a lot of overhead to allocate the object (which has extra locations in it beyond the members), initialize memory, call the constructor, etc.

...

Code Block
scala
scala
object SomeClass {
  val emptyCalendar = {
    val c = Calendar.getInstance(TimeZone.UNKNOWN_ZONE)
    c.clear()
    c
  }
}

def functionThatNeedsANewCalendar = {
  val cal = SomeClass.emptyCalendar.clone.asInstanceOf[Calendar]
  ...
  cal.set(...)
  ...
  cal
}

Examining Bytecode

As is apparent from many of the above suggestions, minimizing allocations is often key to improving Daffodil performance and making profiling less noisy. Often times an allocation will occur but it isn't clear based on the source why such an allocation might be happening. In these cases, it is often necessary to inspect the bytecode. To do so, the use of the javap  function can be invaluable. The following will convert a class to bytecode, including some helpful bytecode interpretations in comments:

...

Code Block
bash
bash
grep -a "new" -n disassembled.txt | grep "java/math/BigInteger"

Profiling & Timing

Often time it is useful to use a profiling to example memory allocations and CPU usage to determine where to target optimizations. However, due to the nested nature of Daffodil parsers/unparser, some profilers can make it difficult to determine how long certain sections of code take, or they incur too make overhead and skew the results. For this reason a speical timer is added to Daffodil's utilties to track sections of code. This timer is the TimeTracker  in Timer.scala. A common use of this timer is to track the time of all the parsers. Do enable this, adjust the parse1()  method in Parser.scala  to like like this:

Code Block
scala
scala
TimeTracker.track(parserName) {
  parse(pstate)
}

Then add this section to the end of however your are trigger parsing (e.g. Daffodil CLI code, unit test, performance rig)

Code Block
scala
scala
TimeTracker.logTimes(LogLevel.Error)

This will result in something that looks like the following, where the time is in seconds, the average is nanoseconds, and count is the number of times that section was executed.

Code Block
text
text
[error] Name                                 Time     Pct  Average    Count
[error] LiteralNilDelimitedEndOfDataParser  3.330  34.03%     4030   826140
[error] StringDelimitedParser               2.455  25.09%     4184   586640
[error] DelimiterTextParser                 1.038  10.61%      879  1180480
[error] SimpleNilOrValueParser              0.985  10.07%     1192   826140
[error] OrderedSeparatedSequenceParser      0.806   8.23%    10232    78720
[error] ElementParser                       0.404   4.13%      342  1180520
[error] DelimiterStackParser                0.308   3.15%      244  1259220
[error] ChoiceParser                        0.226   2.31%     5750    39360
[error] SeqCompParser                       0.113   1.15%      318   354300
[error] ConvertTextNumberParser             0.060   0.61%  1489652       40
[error] OrderedUnseparatedSequenceParser    0.058   0.60%  2922016       20
[error] ConvertTextCombinatorParser         0.000   0.00%     8825       40

This gives a clear breakown of how much time was spent in each parser (excluding nested child parsers) and gives a rough idea of were to focus optimizations. Note that it often sometimes helpto to add additional tracked sections within a parser to determine what parts of a parser are the bottlenecks.