Skip to content

Runtime performance regression compared to XCTest with a factor of 5-10x for certain tests #710

Closed as not planned
@hassila

Description

@hassila

Description

We've recently ported one of our packages to the new testing infrastructure, and it has been overall smooth - we can see one significant performance regression on the actual runtime of the test and have narrowed it down to the use of #expect in two places - I will provide the test code and timing results and sample of the runtime here, but we can work on providing a reduced case if this does not give enough hints on what the underlying problem can be.

The timing measurements we see are:

Release mode (swift test -c release):

XCTest:

Test Case '-[PricingTests.ArbitrageTests testBlackScholesForArbitrage]' passed (3.371 seconds).

New Testing:

􁁛  Test blackScholesForArbitrage() passed after 37.032 seconds.

Debug (swift test):

XCTest:

Test Case '-[PricingTests.ArbitrageTests testBlackScholesForArbitrage]' passed (30.888 seconds).

New Testing:

􁁛  Test blackScholesForArbitrage() passed after 164.550 seconds.

New Testing (with commenting out two #expect in the loops in the sample code provided below):

􁁛  Test blackScholesForArbitrage() passed after 98.767 seconds.

So basically we see a 10x runtime difference in release mode with XCtest winning over the new test framework - and a factor 6 in debug mode. Commenting out the two #expects in the sample below (best guess based on the sample) gave a significant improvement from 164 ---> 98 seconds runtime.

Attaching sample:

Sample.txt

Here is the test code in question for the new testing framework (no changes in the test itself, just using the new framework):

 @Test func blackScholesForArbitrage() throws {
    let allowedDeviation = 5.0e-11
    let sampleSize = 1_000_000

    for _ in 0...sampleSize {
      let S = Double.random(in: 0...1_000)
      let X = Double.random(in: 0...1_000)
      let σ = Double.random(in: 0...2.0)
      let r = Double.random(in: 0...2.0)
      let q = Double.random(in: 0...2.0)
      let t = Double.random(in: 0...10.0)
      let putCallParity = S * .exp(-q * t) - .exp(-r * t) * X
      let sampleDividends: [Pricing.Dividend] = []
      var bsValues: [Pricing.Results] = []
      let commonParams = Pricing.Parameters.Common(
        exerciseType: .european,
        spotPrice: Pricing.SideValues(bid: S, ask: S),
        financingRate: r,
        yield: q,
        timeToMaturity: t,
        dividends: sampleDividends
      )

      let specifics = [
        Pricing.Parameters.Specific(
          marketPrice: Pricing.SideValues(bid: 5.0, ask: 21.0), volatility: σ
        ),
        Pricing.Parameters.Specific(
          marketPrice: Pricing.SideValues(bid: 5.0, ask: 21.0), volatility: σ
        ),
      ]

      let referenceData = [
        Pricing.Parameters.InstrumentReferenceData(instrumentType: .call, strikePrice: X),
        Pricing.Parameters.InstrumentReferenceData(instrumentType: .put, strikePrice: X),
      ]
      let pricingContext = Pricing.Context(
        commonParameters: commonParams,
        reference: referenceData,
        parameters: specifics,
        theoreticalValues: [.fair, .delta, .vega, .rho, .vanna, .gamma]
      )
      BlackScholes.calculate(context: pricingContext, results: &bsValues)
      let call = bsValues[0].fair ?? 0.0
      let put = bsValues[1].fair ?? 0.0

      for (index, result) in bsValues.enumerated() {
        let boundaries = ArbitrageBoundaries.validFairValue(
          commonParameters: commonParams,
          reference: referenceData[index]
        )
        if let fairResult = result.fair {
//          #expect(
//            (boundaries?.contains(fairResult)) != false,
//            "fair value \(fairResult) is not within \(String(describing: boundaries))"
//          )
        } else {
          #expect(result.fair != nil, "No fair result returned")
        }
      }

      #expect(abs(call - put - putCallParity) < allowedDeviation, "\(call) \(put)")
    }

    for _ in 0...sampleSize {
      let S = Double.random(in: 0...1_000)
      let X = Double.random(in: 0...1_000)
      let σ = Double.random(in: 0...2.0)
      let r = Double.random(in: 0...2.0)
      let q = Double.random(in: 0...2.0)
      let t = Double.random(in: 0...10.0)
      let timeToDiv = Double.random(in: 0...t)
      let dividendSize = Double.random(in: 0...S)
      let pvOfDiv = dividendSize * .exp(-r * timeToDiv)
      let putCallParity = (S - pvOfDiv) * .exp(-q * t) - .exp(-r * t) * X
      let sampleDividends: [Pricing.Dividend] = [
        Pricing.Dividend(amount: dividendSize, timeTo: timeToDiv)
      ]
      var bsValues: [Pricing.Results] = []
      let commonParams = Pricing.Parameters.Common(
        exerciseType: .european,
        spotPrice: Pricing.SideValues(bid: S, ask: S),
        financingRate: r,
        yield: q,
        timeToMaturity: t,
        dividends: sampleDividends
      )

      let specifics = [
        Pricing.Parameters.Specific(
          marketPrice: Pricing.SideValues(bid: 5.0, ask: 21.0), volatility: σ
        ),
        Pricing.Parameters.Specific(
          marketPrice: Pricing.SideValues(bid: 5.0, ask: 21.0), volatility: σ
        ),
      ]

      let referenceData = [
        Pricing.Parameters.InstrumentReferenceData(instrumentType: .call, strikePrice: X),
        Pricing.Parameters.InstrumentReferenceData(instrumentType: .put, strikePrice: X),
      ]
      let pricingContext = Pricing.Context(
        commonParameters: commonParams,
        reference: referenceData,
        parameters: specifics,
        theoreticalValues: [.fair]
      )
      BlackScholes.calculate(context: pricingContext, results: &bsValues)
      let call = bsValues[0].fair ?? 0.0
      let put = bsValues[1].fair ?? 0.0

      for (index, result) in bsValues.enumerated() {
        let boundaries = ArbitrageBoundaries.validFairValue(
          commonParameters: commonParams,
          reference: referenceData[index]
        )
        if let fairResult = result.fair {
//          #expect(
//            (boundaries?.contains(fairResult)) != false,
//            "fair value \(fairResult) is not within \(String(describing: boundaries))"
//          )
        } else {
          #expect(result.fair != nil, "No fair result returned")
        }
      }

      #expect(abs(call - put - putCallParity) < allowedDeviation, "\(call) \(put)")
    }
  }

Expected behavior

We expected approximately the same runtime with the new testing framework.

Actual behavior

We saw a 5-10x test runtime regression for certain tests.

Steps to reproduce

No response

swift-testing version/commit hash

Built in with 6.0 release toolchain with Xcode 16

Swift & OS version (output of swift --version ; uname -a)

swift-driver version: 1.115 Apple Swift version 6.0 (swiftlang-6.0.0.9.10 clang-1600.0.26.2)
Target: arm64-apple-macosx15.0
Darwin ice.local 24.0.0 Darwin Kernel Version 24.0.0: Mon Aug 12 20:51:54 PDT 2024; root:xnu-11215.1.10~2/RELEASE_ARM64_T6000 arm64

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug🪲 Something isn't workingperformance🏎️ Performance issues

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions