title: How to scale random simulations with parallelism in Swift?
body: <p>I'm playing around in Swift 5.1 (on the Mac) running little simulations of tennis matches. Naturally part of the simulation is randomly choosing who wins each point.</p>
  
  <p>Below is the relevant part of the code where I do the parallelism.</p>
  
  <pre><code>func combine(result: MatchTally)
  {
      overallTally.add(result: result)
  }
  
  DispatchQueue.concurrentPerform(iterations: cycleCount){iterationNumber in
      var counter = MatchTally()
      for _ in 1...numberOfSimulations
      {
          let result = playMatch(between: playerOne, and: playerTwo)
          counter[result.0, result.1] += 1
      }
      combiningQueue.sync {combine(result: counter)}
  }
  </code></pre>
  
  <p>With an appropriate simulation run count chosen, a single queue takes about 5s. If I set the concurrent queues to 2, the simulation now takes 3.8s per queue (i.e. it took 7.2s). Doubling again to 4 queues results in 4.8s / queue. And finally with 6 queues (the machine is a 6 core Intel i7) things take 5.6s / queue.</p>
  
  <p>For those who need more convincing that this relates to random number generating (I'm using <code>Double.random(0...1)</code>) I replaced the code where most of the random outcomes are generated with a fixed result (I couldn't replace the second place as I still needed a tie-break) and adjusted the number of simulations appropriately, the outcomes were as follows:</p>
  
  <ul>
  <li>1 queue: 5s / queue</li>
  <li>2 queues: 2.7s / queue</li>
  <li>4 queues: 1.9s / queue</li>
  <li>6 queues: 1.7s / queue</li>
  </ul>
  
  <p>So as you can see, it appears that the randomness part is resistant to running in parallel.</p>
  
  <p>I've also tried with drand48() and encountered the same issues. Anybody know whether this is just the way things are?</p>
  
  <p>Xcode 11.3,
  Swift 5.1,
  macOS 10.15.3,
  Mac mini 2018,
  6 core i7 (but have encountered the same thing over the years on different hardware)</p>
  
  <p>For anyone interested in reproducing this themselves, here is some code I created and Alexander added to.</p>
  
  <pre><code>import Foundation
  
  func formatTime(_ date: Date) -&gt; String
  {
      let df = DateFormatter()
      df.dateFormat = "h:mm:ss.SSS"
      return df.string(from: date)
  }
  
  func something(_ iteration: Int)
  {
      var tally = 0.0
      let startTime = Date()
      print("Start #\(iteration) - \(formatTime(startTime))")
  
      for _ in 1...1_000_000
      {
          tally += Double.random(in: 0...100)
  //      tally += 3.5
      }
      let endTime = Date()
      print("End   #\(iteration) - \(formatTime(endTime)) - elapsed: \(endTime.timeIntervalSince(startTime))")
  }
  
  print("Single task performed on main thread")
  something(0)    //  Used to get a baseline for single run
  
  print("\nMultiple tasks performed concurrently")
  DispatchQueue.concurrentPerform(iterations: 5, execute: something)
  </code></pre>
  
  <p>Swapping out the random additive in the loop for a fixed one demonstrates how well the code scales in one scenario, but not the other.</p>