You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@daffodil.apache.org by sl...@apache.org on 2020/01/28 19:46:02 UTC

[incubator-daffodil] branch master updated: Assortment of changes to improve performance

This is an automated email from the ASF dual-hosted git repository.

slawrence pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-daffodil.git


The following commit(s) were added to refs/heads/master by this push:
     new 143731d  Assortment of changes to improve performance
143731d is described below

commit 143731d692363e0a315c560abc3d62060ae3bb86
Author: Steve Lawrence <sl...@apache.org>
AuthorDate: Tue Jan 21 10:16:25 2020 -0500

    Assortment of changes to improve performance
    
    - Optimize how we parse date/times from an XML infoset. Previously we
      attempted to parse a handful of different potential patterns using
      ICU4J SimpleDateFormat.parse(). Profiling show this simple parse
      performed many allocations. Considering that this is a fairly strict
      format we can implement the parsing ourselves, which is much more
      efficient, minimizes allocations, and avoids thread-locals since
      SimpleDateFormat is not thread safe.
    - Instead of creating new calendar every time we parse an XML Date/Time,
      clone an already existing one. The process to initialize a new
      Calendar is pretty expensive, cloning is much more efficient.
    - Skip remapping XML strings if they do not contain any invalid
      characters. This avoids unnecessary string allocations in the common
      case, but does incur some overhead in the rare case when remapping is
      needed.
    - Use the same empty Array of DFADelimiters in the delimiter stack.
      Otherwise we allocate a bunch of empty arrays, and scala even ends up
      allocating even ore stuff to support it (e.g. ClassTags).
    - Fix MaybeBoolean to avoid allocating new MaybeBoolean's
    - Use Array.length == 0 instead of Array.isEmpty in inner loops. Using
      .isEmpty will allocate a scala ArrayOps, which is unnecssary if all
      were doing is checking the length. Note that .size will also allocate
      an ArrayOps.
    - Fix maybeConstant_ in Evaluatable to avoid Maybe allocations
    
    These issues were discovered while profiling an unparse of a CSV schema.
    Performance averaged around 20% faster on my testing.
    
    DAFFODIL-2222, DAFFODIL-1883
---
 .../daffodil/calendar/DFDLCalendarConversion.scala | 366 +++++++++++++++------
 .../scala/org/apache/daffodil/util/Maybe.scala     |   3 +
 .../scala/org/apache/daffodil/util/MaybeInt.scala  |   8 +-
 .../scala/org/apache/daffodil/xml/XMLUtils.scala   |  51 ++-
 .../unparsers/ChoiceAndOtherVariousUnparsers.scala |  11 +-
 .../processors/unparsers/DelimiterUnparsers.scala  |   2 +-
 .../org/apache/daffodil/infoset/InfosetImpl.scala  |   2 +-
 .../apache/daffodil/infoset/InfosetInputter.scala  |   4 +-
 .../processors/DelimiterStackUnparseNode.scala     |   7 +-
 .../apache/daffodil/processors/Evaluatable.scala   |   2 +-
 .../daffodil/processors/unparsers/UState.scala     |   4 +-
 .../calc_value_properties/outputValueCalc2.tdml    |   4 +-
 .../section23/dfdl_functions/Functions.tdml        |   8 +-
 13 files changed, 340 insertions(+), 132 deletions(-)

diff --git a/daffodil-lib/src/main/scala/org/apache/daffodil/calendar/DFDLCalendarConversion.scala b/daffodil-lib/src/main/scala/org/apache/daffodil/calendar/DFDLCalendarConversion.scala
index 0c4ecda..aaa50d2 100644
--- a/daffodil-lib/src/main/scala/org/apache/daffodil/calendar/DFDLCalendarConversion.scala
+++ b/daffodil-lib/src/main/scala/org/apache/daffodil/calendar/DFDLCalendarConversion.scala
@@ -17,33 +17,12 @@
 
 package org.apache.daffodil.calendar
 
+import java.lang.Integer
+
 import com.ibm.icu.util.Calendar
-import com.ibm.icu.text.SimpleDateFormat
+import com.ibm.icu.util.SimpleTimeZone
 import com.ibm.icu.util.TimeZone
 
-import java.text.ParseException
-import java.text.ParsePosition
-
-/**
- * Wrapper arounda SimpleDateFormat and a flag that determines if it expects to
- * parse a timezone or not
- */
-case class DFDLCalendarFormat private (format: SimpleDateFormat, expectsTimezone: Boolean)
-
-object DFDLCalendarFormat {
-  def apply(pattern: String, expectsTimezone: Boolean): DFDLCalendarFormat = {
-    new DFDLCalendarFormat(InfosetSimpleDateFormat(pattern), expectsTimezone)
-  }
-}
-
-object InfosetSimpleDateFormat {
-  def apply(pattern: String): SimpleDateFormat = {
-    val sdf = new SimpleDateFormat(pattern)
-    sdf.setLenient(false)
-    sdf
-  }
-}
-
 object DFDLCalendarConversion {
 
   @inline
@@ -81,6 +60,49 @@ object DFDLCalendarConversion {
     ysign + pad4(Math.abs(y)) + "-" + pad2(m) + "-" + pad2(d)
   }
 
+  /**
+   * Parses a string that begins with the pattern "uuuu-MM-dd" and sets the
+   * appropriate values in the calendar. The year part may be 1 or more digits
+   * (including an optional sign) and must be a valid postive or negative
+   * integer. The month and day parts must be zero padded digits.
+   *
+   * If the pattern is not followed, an IllegalArgumentException is thrown.
+   *
+   * @return if the date part was succesfully parsed, returns a substring of
+   *         the remaining characters
+   */
+  def datePartFromXMLString(string: String, calendar: Calendar): String = {
+    @inline
+    def invalidValue = throw new IllegalArgumentException("Invalid date string: %s".format(string))
+
+    if (string.length == 0) invalidValue
+
+    val endYear =
+      if (string.charAt(0) == '-') {
+        string.indexOf('-', 1) // skip negative sign in negative years
+      } else {
+        string.indexOf('-')
+      }
+
+    if (endYear == -1) invalidValue
+    if (string.length < endYear + 6) invalidValue
+    if (string.charAt(endYear + 3) != '-') invalidValue
+
+    val y = string.substring(0, endYear)
+    val m = string.substring(endYear + 1, endYear + 3)
+    val d = string.substring(endYear + 4, endYear + 6)
+
+    try {
+      calendar.set(Calendar.EXTENDED_YEAR, Integer.parseInt(y))
+      calendar.set(Calendar.MONTH, Integer.parseInt(m) - 1)
+      calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(d))
+    } catch {
+      case _: NumberFormatException => invalidValue
+    }
+
+    string.substring(endYear + 6)
+  }
+
   def timePartToXMLString(dfdlcal: DFDLCalendar): String = {
     val calendar = dfdlcal.calendar
     val h = calendar.get(Calendar.HOUR_OF_DAY)
@@ -91,6 +113,72 @@ object DFDLCalendarConversion {
     pad2(h) + ":" + pad2(m) + ":" + pad2(s) + (if (u != 0) "." + pad3(u) + "000" else "")
   }
 
+  /**
+   * Parses a string that begins with the pattern "HH:mm:ss.SSSSSS" (where the
+   * .SSSSSS is optional) and sets the appropriate values in the calendar. The
+   * hour, minute, and second parts are zero padded two digits. The
+   * milliseconds is everything up to the time zone or end of string if there
+   * is no time zon
+   *
+   * If the pattern is not followed, an IllegalArgumentException is thrown.
+   *
+   * @return if the time part was succesfully parsed, returns a substring of
+   *         the remaining characters
+   */
+  def timePartFromXMLString(string: String, calendar: Calendar): String = {
+    @inline
+    def invalidValue = throw new IllegalArgumentException("Invalid time string: %s".format(string))
+
+    if (string.length < 8) invalidValue
+    if (string.charAt(2) != ':') invalidValue
+    if (string.charAt(5) != ':') invalidValue
+
+    val h = string.substring(0, 2)
+    val m = string.substring(3, 5)
+    val s = string.substring(6, 8)
+
+    val (ms, endTime) =
+      if (string.length > 8) {
+        // must have milliseconds or a time zone
+        if (string.charAt(8) != '.') {
+          // must be a time zone and no milliseconds
+          ("0", 8)
+        } else {
+          // must have milliseconds, maybe a time zone
+          val tzStart = string.indexWhere(c => c == '-' || c == '+' || c == 'Z', 9)
+          if (tzStart == -1) {
+            // no timezone, the rest of string is milliseconds
+            (string.substring(9), string.length)
+          } else {
+            // has timezone, just consume up that
+            (string.substring(9, tzStart), tzStart)
+          }
+        }
+      } else {
+        // no milliseconds or time zone
+        ("0", 8)
+      }
+
+    try {
+      calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(h))
+      calendar.set(Calendar.MINUTE, Integer.parseInt(m))
+      calendar.set(Calendar.SECOND, Integer.parseInt(s))
+      // ICU only supports integer milliseconds precision, which means we can
+      // only support at most 3 digits from the milliseconds field
+      val msDigits = Math.min(ms.length, 3)
+      val msUnscaled = Integer.parseInt(ms.substring(0, msDigits))
+      val msScaled =
+        if (msDigits== 1) msUnscaled * 100
+        else if (msDigits == 2) msUnscaled * 10
+        else msUnscaled
+      calendar.set(Calendar.MILLISECOND, msScaled)
+    } catch {
+      case _: NumberFormatException => invalidValue
+    }
+
+    string.substring(endTime)
+  }
+
   def timeZonePartToXMLString(dfdlcal: DFDLCalendar): String = {
     if (!dfdlcal.hasTimeZone) {
       ""
@@ -111,54 +199,87 @@ object DFDLCalendarConversion {
       s + pad2(h) + ":" + pad2(m)
     }
   }
-}
-
-trait DFDLCalendarConversion {
-
-  val calendarType: String
-
-  protected def fromXMLFormats = new ThreadLocal[Seq[DFDLCalendarFormat]]
 
   /**
-   * Attempts to parse a given date/time string with multiple allowable
-   * formats. If the string is completely parsed with one of the formats, it
-   * returns the resulting Calendar of the parse and the DFDLCalendarFormat
-   * that led to the successful match.
+   * Parses a string that of the pattern [+-]hh:mm(:ss)? and sets the timezone
+   * in the calendar. The hour, minute, and second parts are zero padded two
+   * digits.
+   *
+   * If the pattern is not followed, an IllegalArgumentException is thrown.
+   *
+   * @return if the time part was succesfully parsed, returns a substring of
+   *         the remaining characters
    */
-  protected def parseFromXMLString(string: String): (Calendar, DFDLCalendarFormat) = {
-    // Create strict calendar, initialize with no time zone to ensure timezone
-    // information only comes from the data if it exists
-    val calendar = Calendar.getInstance(TimeZone.UNKNOWN_ZONE)
-    calendar.setLenient(false)
-
-    val successfulFormat = fromXMLFormats.get.find { calendarFormat =>
-
-      val pos = new ParsePosition(0)
-      calendar.clear()
-      calendarFormat.format.parse(string, calendar, pos)
-
-      try {
-        calendar.getTime()
-       
-        if (pos.getIndex() == 0 || pos.getErrorIndex() != -1 || pos.getIndex() != string.length()) {
-          false
+  def timeZonePartFromXMLString(string: String, calendar: Calendar): String = {
+    @inline
+    def invalidValue = throw new IllegalArgumentException("Invalid time zone string: %s".format(string))
+
+    if (string == "") {
+      // no timezone
+      string
+    } else {
+      val firstChar = string.charAt(0)
+      val (timezone, endTimeZone) =
+        if (firstChar == 'Z') {
+          (TimeZone.GMT_ZONE, 1)
         } else {
-          true
-        }
-      } catch {
-        case _: IllegalArgumentException => {
-          // thrown by getTime() when parsed data is not strictly correct
-          false
+          val sign =
+            if (firstChar == '+') 1
+            else if (firstChar == '-') -1
+            else invalidValue
+
+          if (string.length < 6) invalidValue
+          if (string.charAt(3) != ':') invalidValue
+
+          val h = string.substring(1, 3)
+          val m = string.substring(4, 6)
+          val s =
+            if (string.length > 6) {
+              if (string.charAt(6) != ':') invalidValue
+              string.substring(7, 9)
+            } else {
+              "00" 
+            }
+
+          val offsetInMillis = try {
+            val hi = Integer.parseInt(h)
+            val mi = Integer.parseInt(m)
+            val si = Integer.parseInt(s)
+            if (hi < 0 || hi >= 24) invalidValue
+            if (mi < 0 || mi >= 60) invalidValue
+            if (si < 0 || si >= 60) invalidValue
+            sign * (hi * 60 * 60 +  mi * 60 + si) * 1000
+          } catch {
+            case _: NumberFormatException => invalidValue
+          }
+
+          val tz =
+            if (offsetInMillis == 0) TimeZone.GMT_ZONE
+            else new SimpleTimeZone(offsetInMillis, string)
+          val consumed = if (string.length > 6) 9 else 6
+
+          (tz, consumed)
         }
-      }
-    }
 
-    if (successfulFormat.isEmpty) {
-      throw new IllegalArgumentException(
-        """Failed to parse "%s" to an %s""".format(string, calendarType))
+        calendar.setTimeZone(timezone)
+        string.substring(endTimeZone)
     }
+  }
+}
 
-    (calendar, successfulFormat.get)
+trait DFDLCalendarConversion {
+  val calendarType: String
+
+  @inline
+  final protected def invalidCalendar(string: String): Nothing = { 
+    throw new IllegalArgumentException("Failed to parse %s from string: %s".format(calendarType, string))
+  }
+
+  protected val emptyCalendar = {
+    val c = Calendar.getInstance(TimeZone.UNKNOWN_ZONE)
+    c.clear()
+    c.setLenient(false)
+    c
   }
 }
 
@@ -166,23 +287,38 @@ object DFDLDateTimeConversion extends DFDLCalendarConversion {
 
   val calendarType = "xs:dateTime"
 
-  @transient
-  override protected lazy val fromXMLFormats = new ThreadLocal[Seq[DFDLCalendarFormat]] {
-    override def initialValue = {
-      Seq(
-        DFDLCalendarFormat("uuuu-MM-dd'T'HH:mm:ss.SSSSSSxxxxx", true),
-        DFDLCalendarFormat("uuuu-MM-dd'T'HH:mm:ss.SSSSSS", false),
-        DFDLCalendarFormat("uuuu-MM-dd'T'HH:mm:ssxxxxx", true),
-        DFDLCalendarFormat("uuuu-MM-dd'T'HH:mm:ss", false),
-        DFDLCalendarFormat("uuuu-MM-ddxxxxx", true),
-        DFDLCalendarFormat("uuuu-MM-dd", false)
-      )
-    }
-  }
-
+  /**
+   * Supported patterns:
+   *   uuuu-MM-dd'T'HH:mm:ss.SSSSSSxxxxx
+   *   uuuu-MM-dd'T'HH:mm:ss.SSSSSS
+   *   uuuu-MM-dd'T'HH:mm:ssxxxxx
+   *   uuuu-MM-dd'T'HH:mm:ss
+   *   uuuu-MM-ddxxxxx
+   *   uuuu-MM-dd
+   */
   def fromXMLString(string: String): DFDLDateTime = {
-    val (calendar, format) = parseFromXMLString(string)
-    DFDLDateTime(calendar, format.expectsTimezone)
+    val calendar = emptyCalendar.clone().asInstanceOf[Calendar]
+
+    try {
+      val rem1 = DFDLCalendarConversion.datePartFromXMLString(string, calendar)
+      val rem2 = 
+        if (rem1.length > 0 && rem1(0) == 'T') {
+          DFDLCalendarConversion.timePartFromXMLString(rem1.substring(1), calendar)
+        } else {
+          rem1
+        }
+      val rem3 = DFDLCalendarConversion.timeZonePartFromXMLString(rem2, calendar)
+      if (rem3.length > 0) invalidCalendar(string)
+      val hasTimeZone = rem2.length > 0
+
+      // this causes validation of the fields
+      calendar.getTimeInMillis()
+
+      DFDLDateTime(calendar, hasTimeZone)
+    } catch {
+      // thrown by us if a string doesn't match a pattern, or ICU if fields are invalid
+      case _: IllegalArgumentException => invalidCalendar(string)
+    }
   }
 
   def toXMLString(dt: DFDLDateTime): String = {
@@ -197,19 +333,28 @@ object DFDLDateConversion extends DFDLCalendarConversion {
 
   val calendarType = "xs:date"
 
-  @transient
-  override protected lazy val fromXMLFormats = new ThreadLocal[Seq[DFDLCalendarFormat]] {
-    override def initialValue = {
-      Seq(
-        DFDLCalendarFormat("uuuu-MM-ddxxxxx", true),
-        DFDLCalendarFormat("uuuu-MM-dd", false)
-      )
-    }
-  }
-
+  /**
+   * Supported patterns:
+   *   uuuu-MM-ddxxxxx
+   *   uuuu-MM-dd
+   */
   def fromXMLString(string: String): DFDLDate = {
-    val (calendar, format) = parseFromXMLString(string)
-    DFDLDate(calendar, format.expectsTimezone)
+    val calendar = emptyCalendar.clone().asInstanceOf[Calendar]
+
+    try {
+      val rem1 = DFDLCalendarConversion.datePartFromXMLString(string, calendar)
+      val rem2 = DFDLCalendarConversion.timeZonePartFromXMLString(rem1, calendar)
+      if (rem2.length > 0) invalidCalendar(string)
+      val hasTimeZone = rem1.length > 0
+   
+      // this causes validation of the fields
+      calendar.getTimeInMillis()
+
+      DFDLDate(calendar, hasTimeZone)
+    } catch {
+      // thrown by us if a string doesn't match a pattern, or ICU if fields are invalid
+      case _: IllegalArgumentException => invalidCalendar(string)
+    }
   }
 
   def toXMLString(d: DFDLDate): String = {
@@ -222,21 +367,30 @@ object DFDLTimeConversion extends DFDLCalendarConversion {
 
   val calendarType = "xs:time"
 
-  @transient
-  override protected lazy val fromXMLFormats = new ThreadLocal[Seq[DFDLCalendarFormat]] {
-    override def initialValue = {
-      Seq(
-        DFDLCalendarFormat("HH:mm:ss.SSSSSSxxxxx", true),
-        DFDLCalendarFormat("HH:mm:ss.SSSSSS", false),
-        DFDLCalendarFormat("HH:mm:ssxxxxx", true),
-        DFDLCalendarFormat("HH:mm:ss", false)
-      )
-    }
-  }
-
+  /**
+   * Supported patterns:
+   *   HH:mm:ss.SSSSSSxxxxx
+   *   HH:mm:ss.SSSSSS
+   *   HH:mm:ssxxxxx
+   *   HH:mm:ss
+   */
   def fromXMLString(string: String): DFDLTime = {
-    val (calendar, format) = parseFromXMLString(string)
-    DFDLTime(calendar, format.expectsTimezone)
+    val calendar = emptyCalendar.clone().asInstanceOf[Calendar]
+
+    try {
+      val rem1 = DFDLCalendarConversion.timePartFromXMLString(string, calendar)
+      val rem2 = DFDLCalendarConversion.timeZonePartFromXMLString(rem1, calendar)
+      if (rem2.length > 0) invalidCalendar(string)
+      val hasTimeZone = rem1.length > 0
+
+      // this causes validation of the fields
+      calendar.getTimeInMillis()
+
+      DFDLTime(calendar, hasTimeZone)
+    } catch {
+      // thrown by us if a string doesn't match a pattern, or ICU if fields are invalid
+      case _: IllegalArgumentException => invalidCalendar(string)
+    }
   }
 
 
diff --git a/daffodil-lib/src/main/scala/org/apache/daffodil/util/Maybe.scala b/daffodil-lib/src/main/scala/org/apache/daffodil/util/Maybe.scala
index 060e4b7..21bdfd5 100644
--- a/daffodil-lib/src/main/scala/org/apache/daffodil/util/Maybe.scala
+++ b/daffodil-lib/src/main/scala/org/apache/daffodil/util/Maybe.scala
@@ -118,6 +118,9 @@ object Maybe {
   @inline
   final def apply[T <: AnyRef](value: T) = if (value == null) Nope else new Maybe[T](value)
 
+  @inline
+  final def fromMaybeAnyRef[T <: AnyRef](anyref: Maybe[AnyRef]) = Maybe(anyref.v.asInstanceOf[T])
+
   val Nope = new Maybe[Nothing](NopeValue)
 
   /**
diff --git a/daffodil-lib/src/main/scala/org/apache/daffodil/util/MaybeInt.scala b/daffodil-lib/src/main/scala/org/apache/daffodil/util/MaybeInt.scala
index 721c640..d112689 100644
--- a/daffodil-lib/src/main/scala/org/apache/daffodil/util/MaybeInt.scala
+++ b/daffodil-lib/src/main/scala/org/apache/daffodil/util/MaybeInt.scala
@@ -140,7 +140,7 @@ object MaybeChar {
   val Nope = new MaybeChar(undefValue)
 }
 
-final case class MaybeBoolean private (__v: Int) extends AnyVal {
+final class MaybeBoolean private (val __v: Int) extends AnyVal {
   @inline final def get: Boolean = if (isEmpty) noneGet else __v == 1
   //@inline final def getOrElse(alternate: Boolean): Boolean = if (isDefined) get else alternate
   private def noneGet = throw new NoSuchElementException("Nope.get")
@@ -158,10 +158,10 @@ final case class MaybeBoolean private (__v: Int) extends AnyVal {
 object MaybeBoolean {
   private val undefValue = -1
 
-  @inline final def apply(v: Boolean) = new MaybeBoolean(if (v) 1 else 0)
+  @inline final def apply(v: Boolean) = if (v) MaybeBoolean.True else MaybeBoolean.False
 
   val Nope = new MaybeBoolean(undefValue)
-  val True = MaybeBoolean(true)
-  val False = MaybeBoolean(false)
+  val True = new MaybeBoolean(1)
+  val False = new MaybeBoolean(0)
   
 }
diff --git a/daffodil-lib/src/main/scala/org/apache/daffodil/xml/XMLUtils.scala b/daffodil-lib/src/main/scala/org/apache/daffodil/xml/XMLUtils.scala
index 880ee66..71dc223 100644
--- a/daffodil-lib/src/main/scala/org/apache/daffodil/xml/XMLUtils.scala
+++ b/daffodil-lib/src/main/scala/org/apache/daffodil/xml/XMLUtils.scala
@@ -88,6 +88,20 @@ object XMLUtils {
     res
   }
 
+  def needsXMLToPUARemapping(s: String): Boolean = {
+    var i = 0
+    val len = s.length
+    while (i < len) {
+      val v = s.charAt(i).toInt
+      if ((v < 0x20 && !(v == 0xA || v == 0x9)) || (v > 0xD7FF && v < 0xE000) ||
+          (v >= 0xE000 && v <= 0xF8FF) || (v == 0xFFFE) || (v == 0xFFFF) || (v > 0x10FFFF)) {
+        return true
+      }
+      i += 1
+    }
+    false
+  }
+
   /**
    * Reverse of the above method
    */
@@ -106,6 +120,21 @@ object XMLUtils {
     res
   }
 
+  def needsPUAToXMLRemapping(s: String): Boolean = {
+    var i = 0
+    val len = s.length
+    while (i < len) {
+      val v = s.charAt(i).toInt
+      if ((v == 0xD) || // not PUA, but string still needs remapping since CR must be mapped to LF
+          (v >= 0xE000 && v < 0xE020) || (v > 0xE7FF && v < 0xF000) ||
+          (v == 0xF0FE) || (v == 0xF0FF) || (v > 0x10FFFF)) {
+        return true
+      }
+      i += 1
+    }
+    false
+  }
+
   def isLeadingSurrogate(c: Char) = {
     c >= 0xD800 && c <= 0xDBFF
   }
@@ -214,11 +243,29 @@ object XMLUtils {
   }
 
   def remapXMLIllegalCharactersToPUA(dfdlString: String): String = {
-    remapXMLCharacters(dfdlString, remapXMLIllegalCharToPUA(false))
+    if (needsXMLToPUARemapping(dfdlString)) {
+      // This essentially doubles the work if remapping is needed (since we
+      // scan the string once to see if it's needed, then scan again for
+      // remapping). But the common case is that remapping is not needed, so we
+      // only need to scan the string once AND we avoid allocating a new string
+      // with characters remapped.
+      remapXMLCharacters(dfdlString, remapXMLIllegalCharToPUA(false))
+    } else {
+      dfdlString
+    }
   }
 
   def remapPUAToXMLIllegalCharacters(dfdlString: String): String = {
-    remapXMLCharacters(dfdlString, remapPUAToXMLIllegalChar(false))
+    if (needsPUAToXMLRemapping(dfdlString)) {
+      // This essentially doubles the work if remapping is needed (since we
+      // scan the string once to see if it's needed, then scan again for
+      // remapping). But the common case is that remapping is not needed, so we
+      // only need to scan the string once AND we avoid allocating a new string
+      // with characters remapped.
+      remapXMLCharacters(dfdlString, remapPUAToXMLIllegalChar(false))
+    } else {
+      dfdlString
+    }
   }
 
   def coalesceAllAdjacentTextNodes(node: Node): Node = {
diff --git a/daffodil-runtime1-unparser/src/main/scala/org/apache/daffodil/processors/unparsers/ChoiceAndOtherVariousUnparsers.scala b/daffodil-runtime1-unparser/src/main/scala/org/apache/daffodil/processors/unparsers/ChoiceAndOtherVariousUnparsers.scala
index 5790855..c485d0a 100644
--- a/daffodil-runtime1-unparser/src/main/scala/org/apache/daffodil/processors/unparsers/ChoiceAndOtherVariousUnparsers.scala
+++ b/daffodil-runtime1-unparser/src/main/scala/org/apache/daffodil/processors/unparsers/ChoiceAndOtherVariousUnparsers.scala
@@ -17,15 +17,14 @@
 
 package org.apache.daffodil.processors.unparsers
 
-import org.apache.daffodil.processors._
 import org.apache.daffodil.infoset._
 import org.apache.daffodil.processors.RuntimeData
+import org.apache.daffodil.processors._
 import org.apache.daffodil.processors.dfa.DFADelimiter
 import org.apache.daffodil.schema.annotation.props.gen.ChoiceLengthKind
-import org.apache.daffodil.util.Maybe._
 import org.apache.daffodil.util.Maybe
-import org.apache.daffodil.util.MaybeInt
 import org.apache.daffodil.util.Maybe._
+import org.apache.daffodil.util.MaybeInt
 
 class ChoiceCombinatorUnparser(
   mgrd: ModelGroupRuntimeData,
@@ -120,9 +119,9 @@ class DelimiterStackUnparser(
 
   def unparse(state: UState): Unit = {
     // Evaluate Delimiters
-    val init = if (initiatorOpt.isDefined) initiatorOpt.get.evaluate(state) else Array[DFADelimiter]()
-    val sep = if (separatorOpt.isDefined) separatorOpt.get.evaluate(state) else Array[DFADelimiter]()
-    val term = if (terminatorOpt.isDefined) terminatorOpt.get.evaluate(state) else Array[DFADelimiter]()
+    val init = if (initiatorOpt.isDefined) initiatorOpt.get.evaluate(state) else EmptyDelimiterStackUnparseNode.empty
+    val sep = if (separatorOpt.isDefined) separatorOpt.get.evaluate(state) else EmptyDelimiterStackUnparseNode.empty
+    val term = if (terminatorOpt.isDefined) terminatorOpt.get.evaluate(state) else EmptyDelimiterStackUnparseNode.empty
 
     val node = DelimiterStackUnparseNode(init, sep, term)
 
diff --git a/daffodil-runtime1-unparser/src/main/scala/org/apache/daffodil/processors/unparsers/DelimiterUnparsers.scala b/daffodil-runtime1-unparser/src/main/scala/org/apache/daffodil/processors/unparsers/DelimiterUnparsers.scala
index 9da7ce8..a260ba9 100644
--- a/daffodil-runtime1-unparser/src/main/scala/org/apache/daffodil/processors/unparsers/DelimiterUnparsers.scala
+++ b/daffodil-runtime1-unparser/src/main/scala/org/apache/daffodil/processors/unparsers/DelimiterUnparsers.scala
@@ -56,7 +56,7 @@ class DelimiterTextUnparser(override val context: TermRuntimeData, delimiterType
       else localDelimNode.terminator
     }
 
-    if (delimDFAs.isEmpty) Assert.invariantFailed("Expected a delimiter of type " + delimiterType + " on the stack, but was not found.")
+    if (delimDFAs.length == 0) Assert.invariantFailed("Expected a delimiter of type " + delimiterType + " on the stack, but was not found.")
 
     val delimDFA = delimDFAs(0)
 
diff --git a/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/InfosetImpl.scala b/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/InfosetImpl.scala
index 23c1fab..6470584 100644
--- a/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/InfosetImpl.scala
+++ b/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/InfosetImpl.scala
@@ -903,7 +903,7 @@ sealed trait DIElement
    */
   final def maybeIsNilled: MaybeBoolean = {
     if (!_isNilledSet) MaybeBoolean.Nope
-    MaybeBoolean(_isNilled)
+    else MaybeBoolean(_isNilled)
   }
 
   /**
diff --git a/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/InfosetInputter.scala b/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/InfosetInputter.scala
index e026a56..5931851 100644
--- a/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/InfosetInputter.scala
+++ b/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/InfosetInputter.scala
@@ -91,7 +91,9 @@ abstract class InfosetInputter
 
   def isInitialized = isInitialized_
 
-  var tunable = DaffodilTunables()
+  // Should not need to be initialized for performance reasons, this will be
+  // set to an already allocated tunable when initialize() is called
+  var tunable: DaffodilTunables = _
 
   /**
    * Return the current infoset inputter event type
diff --git a/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/DelimiterStackUnparseNode.scala b/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/DelimiterStackUnparseNode.scala
index 3131cae..ae3b1ae 100644
--- a/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/DelimiterStackUnparseNode.scala
+++ b/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/DelimiterStackUnparseNode.scala
@@ -18,9 +18,12 @@
 package org.apache.daffodil.processors
 
 import org.apache.daffodil.processors.dfa.DFADelimiter
+import org.apache.daffodil.util.Maybe
+import org.apache.daffodil.util.Maybe.Nope
 
 object EmptyDelimiterStackUnparseNode {
-  val node = new DelimiterStackUnparseNode(Array(), Array(), Array())
+  val empty = Array[DFADelimiter]()
+  val node = new DelimiterStackUnparseNode(empty, empty, empty)
   def apply() = node
 }
 
@@ -30,7 +33,7 @@ object DelimiterStackUnparseNode {
     initiator: Array[DFADelimiter],
     separator: Array[DFADelimiter],
     terminator: Array[DFADelimiter]): DelimiterStackUnparseNode = {
-    if (initiator.isEmpty && terminator.isEmpty && separator.isEmpty) EmptyDelimiterStackUnparseNode()
+    if (initiator.length == 0 && terminator.length == 0 && separator.length == 0) EmptyDelimiterStackUnparseNode()
     else new DelimiterStackUnparseNode(initiator, separator, terminator)
   }
 
diff --git a/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/Evaluatable.scala b/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/Evaluatable.scala
index 79f3718..daf96a1 100644
--- a/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/Evaluatable.scala
+++ b/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/Evaluatable.scala
@@ -285,7 +285,7 @@ abstract class Evaluatable[+T <: AnyRef](protected val ci: DPathCompileInfo, qNa
   /**
    * Preferred for use in the runtime.
    */
-  @inline final def maybeConstant = constValue_.asInstanceOf[Maybe[T]]
+  @inline final def maybeConstant = Maybe.fromMaybeAnyRef[T](constValue_)
   @inline final def isConstant = constValue_.isDefined
   @inline final def constValue = maybeConstant.get
 
diff --git a/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/unparsers/UState.scala b/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/unparsers/UState.scala
index a2462fa..adf52e3 100644
--- a/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/unparsers/UState.scala
+++ b/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/unparsers/UState.scala
@@ -409,7 +409,7 @@ final class UStateForSuspension(
   override def localDelimiters = delimiterStackMaybe.get.top
   override def allTerminatingMarkup = {
     delimiterStackMaybe.get.iterator.flatMap { dnode =>
-      (dnode.separator.toList ++ dnode.terminator.toList)
+      dnode.separator ++ dnode.terminator
     }.toList
   }
 
@@ -581,7 +581,7 @@ final class UStateMain private (
   override def localDelimiters = delimiterStack.top
   override def allTerminatingMarkup = {
     delimiterStack.iterator.flatMap { dnode =>
-      (dnode.separator.toList ++ dnode.terminator.toList)
+      dnode.separator ++ dnode.terminator
     }.toList
   }
 
diff --git a/daffodil-test/src/test/resources/org/apache/daffodil/section17/calc_value_properties/outputValueCalc2.tdml b/daffodil-test/src/test/resources/org/apache/daffodil/section17/calc_value_properties/outputValueCalc2.tdml
index 8ff943c..dd8ed02 100644
--- a/daffodil-test/src/test/resources/org/apache/daffodil/section17/calc_value_properties/outputValueCalc2.tdml
+++ b/daffodil-test/src/test/resources/org/apache/daffodil/section17/calc_value_properties/outputValueCalc2.tdml
@@ -371,8 +371,8 @@
           <xs:element name="yi" type="xs:string" dfdl:length="5" dfdl:lengthKind="explicit"/>
           <xs:element name="zi" type="xs:string" dfdl:length="5" dfdl:lengthKind="explicit"/>
 
-          <xs:element name="x" type="xs:date" dfdl:lengthKind="delimited" dfdl:representation="text" dfdl:calendarPattern="EEEE MMMM yyyy" dfdl:calendarLanguage="{ ../xi }" dfdl:outputValueCalc="{ xs:date(fn:concat('2017-8-',dfdl:valueLength(../y, 'bytes'))) }" dfdl:calendarPatternKind="explicit" />
-          <xs:element name="y" type="xs:date" dfdl:lengthKind="delimited" dfdl:representation="text" dfdl:calendarPattern="EEEE MMMM yyyy" dfdl:calendarLanguage="{ ../yi }" dfdl:outputValueCalc="{ xs:date(fn:concat('1986-6-',dfdl:valueLength(../z, 'bytes'))) }" dfdl:calendarPatternKind="explicit" />
+          <xs:element name="x" type="xs:date" dfdl:lengthKind="delimited" dfdl:representation="text" dfdl:calendarPattern="EEEE MMMM yyyy" dfdl:calendarLanguage="{ ../xi }" dfdl:outputValueCalc="{ xs:date(fn:concat('2017-08-',dfdl:valueLength(../y, 'bytes'))) }" dfdl:calendarPatternKind="explicit" />
+          <xs:element name="y" type="xs:date" dfdl:lengthKind="delimited" dfdl:representation="text" dfdl:calendarPattern="EEEE MMMM yyyy" dfdl:calendarLanguage="{ ../yi }" dfdl:outputValueCalc="{ xs:date(fn:concat('1986-06-',dfdl:valueLength(../z, 'bytes'))) }" dfdl:calendarPatternKind="explicit" />
           <xs:element name="z" type="xs:date" dfdl:lengthKind="delimited" dfdl:representation="text" dfdl:calendarPattern="EEEE MMMM yyyy" dfdl:calendarLanguage="{ ../zi }" dfdl:calendarPatternKind="explicit" dfdl:encoding="UTF-8" />
         </xs:sequence>
       </xs:complexType>
diff --git a/daffodil-test/src/test/resources/org/apache/daffodil/section23/dfdl_functions/Functions.tdml b/daffodil-test/src/test/resources/org/apache/daffodil/section23/dfdl_functions/Functions.tdml
index 150aec9..5da7406 100644
--- a/daffodil-test/src/test/resources/org/apache/daffodil/section23/dfdl_functions/Functions.tdml
+++ b/daffodil-test/src/test/resources/org/apache/daffodil/section23/dfdl_functions/Functions.tdml
@@ -8315,7 +8315,7 @@
     </tdml:document>
     <tdml:errors>
       <tdml:error>Parse Error</tdml:error>
-      <tdml:error>"02-03-1998T12:30:34"</tdml:error>
+      <tdml:error>02-03-1998T12:30:34</tdml:error>
     </tdml:errors>
   </tdml:parserTestCase>
   
@@ -8532,7 +8532,7 @@
     </tdml:document>
     <tdml:errors>
       <tdml:error>Parse Error</tdml:error>
-      <tdml:error>"03:61:00"</tdml:error>
+      <tdml:error>03:61:00</tdml:error>
     </tdml:errors>
   </tdml:parserTestCase>
   
@@ -8552,7 +8552,7 @@
     </tdml:document>
     <tdml:errors>
       <tdml:error>Parse Error</tdml:error>
-      <tdml:error>"03:59:61"</tdml:error>
+      <tdml:error>03:59:61</tdml:error>
     </tdml:errors>
   </tdml:parserTestCase>
   
@@ -8572,7 +8572,7 @@
     </tdml:document>
     <tdml:errors>
       <tdml:error>Parse Error</tdml:error>
-      <tdml:error>"five o'clock"</tdml:error>
+      <tdml:error>five o'clock</tdml:error>
     </tdml:errors>
   </tdml:parserTestCase>