# Performance of Julia as a general-purpose programming language

**URL:** https://discourse.julialang.org/t/performance-of-julia-as-a-general-purpose-programming-language/6995
**Category:** Performance
**Created:** [November 10, 2017, 5:59pm UTC](https://discourse.julialang.org/t/performance-of-julia-as-a-general-purpose-programming-language/6995 "2017-11-10T17:59:55Z")
**Posts on this page:** 11
**Page:** 2

<div class="post-metadata">

### Author: ![bernhard](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/bernhard/32/2619_2.png) [@bernhard](https://discourse.julialang.org/u/bernhard)
#### Post date: [November 16, 2017, 9:50am UTC](https://discourse.julialang.org/t/performance-of-julia-as-a-general-purpose-programming-language/6995/21 "2017-11-16T09:50:30Z")

</div>

Thanks.

I realize now, there is actually a ton of literature and SO questions on those topics.  
[http://eternallyconfuzzled.com/arts/jsw\_art\_rand.aspx](http://eternallyconfuzzled.com/arts/jsw_art_rand.aspx)

> <https://stackoverflow.com/questions/11758809/what-is-the-optimal-algorithm-for-generating-an-unbiased-random-integer-within-a>

---

<div class="post-metadata">

### Author: ![rfourquet](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/rfourquet/32/3610_2.png) [@rfourquet](https://discourse.julialang.org/u/rfourquet)
#### Post date: [November 16, 2017, 12:43pm UTC](https://discourse.julialang.org/t/performance-of-julia-as-a-general-purpose-programming-language/6995/22 "2017-11-16T12:43:40Z")

</div>

> [@bernhard](#):
>
> It would actually be interesting to see if my simple approach is still faster even for something like:
> 
> rand(231:33:423)

Actually, the base library does this in two steps: generate a random integer `i` in `1:length(231:33:423)`, and then indexing this `StepRange` at `i`, i.e. `(231:33:423)[i]`. So your approach generalizes very easily too. But as @bkamins nicely explained, this is not as perfectly distributed as the base method.

---

<div class="post-metadata">

### Author: ![stevengj](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stevengj/32/71_2.png) [@stevengj](https://discourse.julialang.org/u/stevengj)
#### Post date: [November 16, 2017, 1:09pm UTC](https://discourse.julialang.org/t/performance-of-julia-as-a-general-purpose-programming-language/6995/23 "2017-11-16T13:09:07Z")

</div>

> [@Tamas\_Papp](#):
>
> Can you please elaborate on this? This would be useful to know more about, but I could not find it in the documentation.

This is a consequence of how floating-point numbers work, not of how `rand` is implemented. [A `Float64` is a](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) sign bit, 11 exponent bits, and a 52-bit significand (plus an implicit 1 at the beginning). For normalized values in [0,1), the sign bit and the exponent bits are fixed. So, it is only the significand bits that are random.

---

<div class="post-metadata">

### Author: ![Tamas\_Papp](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tamas_papp/32/25949_2.png) [@Tamas\_Papp](https://discourse.julialang.org/u/Tamas_Papp)
#### Post date: [November 16, 2017, 1:15pm UTC](https://discourse.julialang.org/t/performance-of-julia-as-a-general-purpose-programming-language/6995/24 "2017-11-16T13:15:56Z")

</div>

What was surprising to me is that even `rand(UInt64)` will get 52 random bits _twice_, then `⊻` them. I thought it would get 64 random bits directly.

---

<div class="post-metadata">

### Author: ![bkamins](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/bkamins/32/208538_2.png) [@bkamins](https://discourse.julialang.org/u/bkamins)
#### Post date: [November 16, 2017, 1:49pm UTC](https://discourse.julialang.org/t/performance-of-julia-as-a-general-purpose-programming-language/6995/25 "2017-11-16T13:49:52Z")

</div>

Yes. But in [0,1] they are not distributed uniformly. They are distributed uniformly when you consider `Close1Open2` interval.

You can see it by running:

```julia
[mean(bits(rand())[60]-'0' for i in 1:10^6) for i in 13:64]

```

and

```julia
[mean(bits(rand(Base.Random.Close1Open2))[60]-'0' for i in 1:10^6) for i in 13:64]

```

---

<div class="post-metadata">

### Author: ![rfourquet](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/rfourquet/32/3610_2.png) [@rfourquet](https://discourse.julialang.org/u/rfourquet)
#### Post date: [November 16, 2017, 4:20pm UTC](https://discourse.julialang.org/t/performance-of-julia-as-a-general-purpose-programming-language/6995/26 "2017-11-16T16:20:17Z")

</div>

It’s because the dSFMT library produces natively 52 random bits at a time. Some time ago I had experimented using SFMT instead, which produces directly 64 bits (or 128), but the benefits were not substantial compared to base’s approach (of course I may have had my benchmark wrong).

---

<div class="post-metadata">

### Author: ![Seif\_Shebl](https://avatars.discourse-cdn.com/v4/letter/s/eada6e/32.png) [@Seif\_Shebl](https://discourse.julialang.org/u/Seif_Shebl)
#### Post date: [November 17, 2017, 5:17pm UTC](https://discourse.julialang.org/t/performance-of-julia-as-a-general-purpose-programming-language/6995/27 "2017-11-17T17:17:02Z")

</div>

Actually, nothing stops you in Julia from achieving `1X` C speed. You just need to be more careful and limit the amount of dynamism in your code. As others pointed out, `SVectors` are your best shoot here. In the following code, I used `SVectors` and only modified two lines; line 73 now reads `trunc(Int, 1+rand()*game.nbFreeMoves) ` and line 80 contains a ternary condition which evaluates in both cases to the same function, I removed the condition. I also allowed inlining of most functions via the `@inline` macro.

The following code executes at the same speed as the C++ version on my machine.

```
const NB_GAMES = 1000

using StaticArrays
const EYE9 = eye(SMatrix{9, 9, Int})
const BOARD_LUT = SVector(EYE9, 2*EYE9)

struct Tictactoe
  board::SVector{9,Int}
  nbFreeMoves :: Int
  currentPlayer :: Int
  nextPlayer :: Int
end

Tictactoe() = Tictactoe( [0, 0, 0, 0, 0, 0, 0, 0, 0] , 9, 1, 2)

@inline function computeFinish(b::SVector{9,Int}, nbFreeMoves::Int)
  if b[1] != 0 && b[1] == b[2] == b[3]
    true, b[1]
  elseif b[4] != 0 && b[4] == b[5] == b[6]
    true, b[4]
  elseif b[7] != 0 && b[7] == b[8] == b[9]
    true, b[7]
  elseif b[1] != 0 && b[1] == b[4] == b[7]
    true, b[1]
  elseif b[2] != 0 && b[2] == b[5] == b[8]
    true, b[2]
  elseif b[3] != 0 && b[3] == b[6] == b[9]
    true, b[3]
  elseif b[1] != 0 && b[1] == b[5] == b[9]
    true, b[1]
  elseif b[3] != 0 && b[3] == b[5] == b[7]
    true, b[1]
  elseif nbFreeMoves == 0 # no winner, no empty cell -> draw
    true, 0
  else # no winner, empty cell -> game not finished
    false, 0
  end
end

@inline function findIndex(board::SVector{9,Int}, moveIndex::Int)
  nbFree = 0
  k = 1
  while k<=9 
    if board[k] == 0
      nbFree += 1
      if nbFree == moveIndex
        break
      end
    end
    k += 1
  end
  k
end

@inline function playIndex(game::Tictactoe, moveIndex::Int)
  k = findIndex(game.board, moveIndex)
  if k <= 9
    board = game.board + BOARD_LUT[game.currentPlayer][k,:]
    Tictactoe(board, game.nbFreeMoves-1, game.nextPlayer, game.currentPlayer)
  else
    game
  end
end

abstract type Bot end

@inline function runGame(game::Tictactoe, bot1::Bot, bot2::Bot)
  while true
    finished, winner = computeFinish(game.board, game.nbFreeMoves)
    if finished
      return winner
    end
    moveIndex = genmove(game, bot1) # game.currentPlayer == 1, genmove(game, bot1), genmove(game, bot2) 
    game = playIndex(game, moveIndex)
  end
end

struct RandomBot <: Bot end

@inline genmove(game::Tictactoe, bot::RandomBot) = trunc(Int, 1+rand()*game.nbFreeMoves) # ceil(Int,rand()*game.nbFreeMoves)

struct McBot <: Bot
  maxSims::Int
end

function evalMove(game::Tictactoe, moveIndex::Int, nbSims::Int)
  bot = RandomBot()
  player = game.currentPlayer
  game1 = playIndex(game, moveIndex)
  evalMove1(unused) = runGame(game1, bot, bot) == player ? 1 : 0
  mapreduce(evalMove1, (+), 0, 1:nbSims)
end

function genmove(game::Tictactoe, bot::McBot)
  nbMoves = game.nbFreeMoves
  nbMoveSims = max(1, div(bot.maxSims, nbMoves))
  simulate(moveIndex) = evalMove(game, moveIndex, nbMoveSims)
  wins = map(simulate, [moveIndex for moveIndex in 1:nbMoves])     
  indmax(wins)
end

@inline function runGames(game::Tictactoe, bot1::Bot, bot2::Bot, nbGames::Int)
  res = [0, 0, 0]
  for _ in 1:nbGames
    winner = 1 + runGame(game, bot1, bot2)                     
    res[winner] += 1
  end
  res
end

function runXp(nbSims::Int)
  game = Tictactoe()
  botA = RandomBot()
  botB = McBot(nbSims)
  resAB = runGames(game, botA, botB, NB_GAMES)
  resBA = runGames(game, botB, botA, NB_GAMES)
  println("RandomBot McBot ", nbSims, " ", NB_GAMES, " ", 
          resAB[1], " ", resAB[2], " ", resAB[3], " ", 
          resBA[1], " ", resBA[2], " ", resBA[3], " ", 
          resAB[1]+resBA[1], " ", resAB[2]+resBA[3], " ", resAB[3]+resBA[2])
end

println("botA botB nbSims nbGames drawsA1B2 winsA1 winsB2 drawsB1A2 winsB1 winsA2 draws winsA winsB")
vecNbSims = [1, 10, 100, 1000, 10000]
@time map(runXp, vecNbSims)

```

---

<div class="post-metadata">

### Author: ![juliendehos](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/juliendehos/32/2549_2.png) [@juliendehos](https://discourse.julialang.org/u/juliendehos)
#### Post date: [November 17, 2017, 9:47pm UTC](https://discourse.julialang.org/t/performance-of-julia-as-a-general-purpose-programming-language/6995/28 "2017-11-17T21:47:35Z")

</div>

Line 80, the condition is needed because `runGame` is also used in `runGames` (with different bots). Inlining works great: the SVector version is almost as fast as the C++ version. Thanks.  
Is there a way to predict which function will be interesting to inline ?

---

<div class="post-metadata">

### Author: ![Seif\_Shebl](https://avatars.discourse-cdn.com/v4/letter/s/eada6e/32.png) [@Seif\_Shebl](https://discourse.julialang.org/u/Seif_Shebl)
#### Post date: [November 18, 2017, 7:04pm UTC](https://discourse.julialang.org/t/performance-of-julia-as-a-general-purpose-programming-language/6995/29 "2017-11-18T19:04:51Z")

</div>

Oh, yes I missed this.

Well, you can still beat C++ using only `SVector` without `struct`. The following version is `5%` faster than C++. Enjoy.

```
const NB_boardS = 1000

using StaticArrays
const EYE9 = eye(SMatrix{9, 9, Int})
const BOARD_LUT = SVector(EYE9, 2*EYE9)

const vec9 = SVector{9,Int}
const vec3 = SVector{3,Int}

@inline function computeFinish(b::SVector{9,Int}, nbFreeMoves::Int)
  if b[1] != 0 && b[1] == b[2] == b[3]
    true, b[1]
  elseif b[4] != 0 && b[4] == b[5] == b[6]
    true, b[4]
  elseif b[7] != 0 && b[7] == b[8] == b[9]
    true, b[7]
  elseif b[1] != 0 && b[1] == b[4] == b[7]
    true, b[1]
  elseif b[2] != 0 && b[2] == b[5] == b[8]
    true, b[2]
  elseif b[3] != 0 && b[3] == b[6] == b[9]
    true, b[3]
  elseif b[1] != 0 && b[1] == b[5] == b[9]
    true, b[1]
  elseif b[3] != 0 && b[3] == b[5] == b[7]
    true, b[1]
  elseif nbFreeMoves == 0 # no winner, no empty cell -> draw
    true, 0
  else # no winner, empty cell -> game not finished
    false, 0
  end
end

@inline function findIndex(board::SVector{9,Int}, moveIndex::Int)
  nbFree = 0
  k = 1
  while k<=9 
    if board[k] == 0
      nbFree += 1
      if nbFree == moveIndex
        break
      end
    end
    k += 1
  end
  k
end

@inline function playIndex(board, last3, moveIndex::Int)
  k = findIndex(board, moveIndex)
  if k <= 9
    board = board + BOARD_LUT[last3[2]][k,:]
    board, vec3(last3[1]-1,last3[3],last3[2]) 
  else
    board, last3
  end
end

abstract type Bot end

@inline function runGame(board, last3, bot1::Bot, bot2::Bot)
  while true
    finished, winner = computeFinish(board, last3[1])
    if finished
      return winner
    end
    moveIndex = last3[2] == 1 ? genmove(board, last3, bot1) : genmove(board, last3, bot2) 
    board, last3 = playIndex(board, last3, moveIndex)
  end
end

struct RandomBot <: Bot end

@inline genmove(board, last3, bot::RandomBot) = trunc(Int, 1+rand()*last3[1]) 

struct McBot <: Bot
  maxSims::Int
end

function evalMove(board, last3, moveIndex::Int, nbSims::Int)
  bot = RandomBot()
  player = last3[2]
  board1,vvv3 = playIndex(board, last3, moveIndex)
  evalMove1(unused) = runGame(board1, vvv3, bot, bot) == player ? 1 : 0
  mapreduce(evalMove1, (+), 0, 1:nbSims)
end

function genmove(board, last3, bot::McBot)
  nbMoves = last3[1]
  nbMoveSims = max(1, div(bot.maxSims, nbMoves))
  simulate(moveIndex) = evalMove(board, last3, moveIndex, nbMoveSims)
  wins = map(simulate, [moveIndex for moveIndex in 1:nbMoves]) # 
  indmax(wins)
end

function runGames(board, last3, bot1::Bot, bot2::Bot, nbboards::Int)
  res = [0, 0, 0]
  for _ in 1:nbboards
    winner = 1 + runGame(board, last3, bot1, bot2) # 
    res[winner] += 1
  end
  res
end

function runXp(nbSims::Int)
  board = vec9(0,0,0,0,0,0,0,0,0)
  last3 = vec3(9,1,2)
  botA = RandomBot()
  botB = McBot(nbSims)
  resAB = runGames(board, last3, botA, botB, NB_boardS)
  resBA = runGames(board, last3, botB, botA, NB_boardS)
  println("RandomBot McBot ", nbSims, " ", NB_boardS, " ", 
          resAB[1], " ", resAB[2], " ", resAB[3], " ", 
          resBA[1], " ", resBA[2], " ", resBA[3], " ", 
          resAB[1]+resBA[1], " ", resAB[2]+resBA[3], " ", resAB[3]+resBA[2])
end

println("botA botB nbSims nbboards drawsA1B2 winsA1 winsB2 drawsB1A2 winsB1 winsA2 draws winsA winsB")
vecNbSims = [1, 10, 100, 1000, 10000]
@time map(runXp, vecNbSims)

```

---

<div class="post-metadata">

### Author: ![Elrod](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/elrod/32/22461_2.png) [@Elrod](https://discourse.julialang.org/u/Elrod)
#### Post date: [November 19, 2017, 1:43am UTC](https://discourse.julialang.org/t/performance-of-julia-as-a-general-purpose-programming-language/6995/30 "2017-11-19T01:43:32Z")

</div>

Awesome work!

I remember in the benchmark thread, many said there’s more variability between compilers than between Julia and C/Fortran/&Co.

So, testing your code (following compiling via `runXp(1)`) on my machine:

```julia
julia> versioninfo()
Julia Version 0.6.1
Commit 0d7248e* (2017-10-24 22:15 UTC)
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: AMD Ryzen Threadripper 1950X 16-Core Processor
  WORD_SIZE: 64
  BLAS: libopenblas (ZEN)
  LAPACK: liblapack
  LIBM: libopenlibm
  LLVM: libLLVM-3.9.1 (ORCJIT, generic)

julia> @time map(runXp, vecNbSims);
RandomBot McBot 1 1000 25 301 674 33 861 106 58 407 1535
RandomBot McBot 10 1000 22 265 713 24 934 42 46 307 1647
RandomBot McBot 100 1000 7 115 878 7 982 11 14 126 1860
RandomBot McBot 1000 1000 3 66 931 0 1000 0 3 66 1931
RandomBot McBot 10000 1000 3 55 942 0 1000 0 3 55 1942
  7.797082 seconds (100.84 k allocations: 12.569 MiB, 0.11% gc time)

```

Testing `clang++-4.0`:

```bash
$ clang++-4.0 -o no_lam_clang tictactoe_cmp.cpp -march=native -Ofast -std=c++11
$ time ./no_lam_clang
botA botB nbSims nbGames drawsA1B2 winsA1 winsB2 drawsB1A2 winsB1 winsA2 draws winsA winsB
RandomBot McBot 1 1000 84 441 475 9 922 69 93 510 1397
RandomBot McBot 10 1000 90 399 511 9 938 53 99 452 1449
RandomBot McBot 100 1000 49 217 734 5 982 13 54 230 1716
RandomBot McBot 1000 1000 28 163 809 3 990 7 31 170 1799
RandomBot McBot 10000 1000 22 153 825 4 988 8 26 161 1813

real	0m27.158s
user	0m27.155s
sys	0m0.000s

```

Makes me wonder if I did something wrong with clang???  
If not: that is quite the trouncing by Julia as an llvm frontend!

```bash
$ g++ -o no_lam_5_4 tictactoe_cmp.cpp -march=native -Ofast -std=c++11
$ time ./no_lam_5_4
botA botB nbSims nbGames drawsA1B2 winsA1 winsB2 drawsB1A2 winsB1 winsA2 draws winsA winsB
RandomBot McBot 1 1000 111 413 476 1 917 82 112 495 1393
RandomBot McBot 10 1000 87 416 497 6 948 46 93 462 1445
RandomBot McBot 100 1000 53 247 700 4 986 10 57 257 1686
RandomBot McBot 1000 1000 28 154 818 6 991 3 34 157 1809
RandomBot McBot 10000 1000 35 132 833 4 991 5 39 137 1824

real	0m8.165s
user	0m8.159s
sys	0m0.004s
$ g++-7.2 -o no_lam tictactoe_cmp.cpp -march=native -Ofast
$ time ./no_lam
botA botB nbSims nbGames drawsA1B2 winsA1 winsB2 drawsB1A2 winsB1 winsA2 draws winsA winsB
RandomBot McBot 1 1000 85 419 496 6 927 67 91 486 1423
RandomBot McBot 10 1000 81 432 487 8 935 57 89 489 1422
RandomBot McBot 100 1000 58 243 699 7 990 3 65 246 1689
RandomBot McBot 1000 1000 42 148 810 6 987 7 48 155 1797
RandomBot McBot 10000 1000 29 129 842 8 984 8 37 137 1826

real	0m8.028s
user	0m8.026s
sys	0m0.000s

```

gcc (versions 5.4 and 7.2) did much better, but still not as fast as Julia.

Did I do something wrong with clang – taking about 3.5 times longer than Julia?  
Does that imply anything about the future of Julia with newer versions of llvm?

---

<div class="post-metadata">

### Author: ![juliendehos](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/juliendehos/32/2549_2.png) [@juliendehos](https://discourse.julialang.org/u/juliendehos)
#### Post date: [November 19, 2017, 10:28am UTC](https://discourse.julialang.org/t/performance-of-julia-as-a-general-purpose-programming-language/6995/31 "2017-11-19T10:28:56Z")

</div>

@Seif_Shebl: yes, but to be fair, we should implement this in the C++ code too.

@Elrod: same problem on my machine (linux, gcc 7.2 = 12s, clang 4.0.1 = 36s). I don’t know why…

[Previous page](https://discourse.julialang.org/t/performance-of-julia-as-a-general-purpose-programming-language/6995.md?page=1)
