# Ray Tracing in a week-end - Julia vs SIMD-optimized C++

**URL:** https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958
**Category:** Performance
**Tags:** gpu, graphics, benchmark, pluto, staticarrays
**Created:** [December 11, 2021, 11:58pm UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958 "2021-12-11T23:58:13Z")
**Posts on this page:** 20
**Page:** 2

<div class="post-metadata">

### Author: ![lmiq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lmiq/32/18314_2.png) [@lmiq](https://discourse.julialang.org/u/lmiq)
#### Post date: [December 13, 2021, 12:06am UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958/21 "2021-12-13T00:06:19Z")

</div>

If instead of a abstract type of material you use a union type, using:

```julia
begin
	struct NoMaterial end
	const _no_material = NoMaterial()
	const _y_up = @SVector[0f0,1f0,0f0]
end

struct Metal
	albedo::SVector{3,Float32}
	fuzz::Float32 # how big the sphere used to generate fuzzy reflection rays. 0=none
	Metal(a,f=0.0) = new(a,f)
end

struct Lambertian
	albedo::SVector{3,Float32}
end

struct Dielectric
	ir::Float32 # index of refraction, i.e. η.
end

const Material = Union{NoMaterial,Metal,Lambertian,Dielectric}

```

and _make all structs immutable_, you get almost no allocations in the first `render` function:

```julia
julia> c = default_camera()
Camera(Float32[0.0, 0.0, 0.0], Float32[-1.7777778, -1.0, -1.0], Float32[3.5555556, 0.0, 0.0], Float32[0.0, 2.0, 0.0], Float32[1.0, 0.0, 0.0], Float32[0.0, 1.0, 0.0], Float32[0.0, 0.0, 1.0], 0.0f0)

julia> s = scene_4_spheres()
HittableList(Hittable[Sphere(Float32[0.0, 0.0, -1.0], 0.5f0, Lambertian(Float32[0.7, 0.3, 0.3])), Sphere(Float32[0.0, -100.5, -1.0], 100.0f0, Lambertian(Float32[0.8, 0.8, 0.0])), Sphere(Float32[-1.0, 0.0, -1.0], 0.5f0, Metal(Float32[0.8, 0.8, 0.8], 0.3f0)), Sphere(Float32[1.0, 0.0, -1.0], 0.5f0, Metal(Float32[0.8, 0.6, 0.2], 0.8f0))])

julia> @benchmark render($s,$c, 96, 16)
BenchmarkTools.Trial: 187 samples with 1 evaluation.
 Range (min … max): 26.035 ms … 36.614 ms ┊ GC (min … max): 0.00% … 0.00%
 Time (median): 26.559 ms ┊ GC (median): 0.00%
 Time (mean ± σ): 26.749 ms ± 929.174 μs ┊ GC (mean ± σ): 0.00% ± 0.00%

    ▃▂▁▁▄▃▅█ ▆▂▁▁ ▁                                            
  ▆▃█████████████▄▅█▄▄▄▄▁▃▁▃▁▃▃▄▃▃▁▁▁▁▁▁▁▃▁▃▁▁▁▁▁▁▁▁▃▃▃▄▁▁▁▁▁▃ ▃
  26 ms Histogram: frequency by time 29.2 ms <

 Memory estimate: 60.80 KiB, allocs estimate: 2.

```

This means almost certainly that we got rid of dynamic dispatch and type instabilities. The performance in this small example does not improve (actually it is slightly worse here), but for a larger problem that may be much better. To test.

The use of a union instead of an abstract type is another workaround to suggest to the compiler to do automatic union splitting, but of course it is again less elegant.

For the records, with the abstract material type, I had:

```julia
julia> @benchmark render($s,$c, 96, 16)
BenchmarkTools.Trial: 240 samples with 1 evaluation.
 Range (min … max): 19.864 ms … 29.133 ms ┊ GC (min … max): 0.00% … 0.00%
 Time (median): 20.285 ms ┊ GC (median): 0.00%
 Time (mean ± σ): 20.910 ms ± 1.521 ms ┊ GC (mean ± σ): 1.83% ± 4.79%

  ██▄▂▂▃▆▁ ▁                                                   
  ██████████▇▆█▁▆▆▄▄▆▄▄█▆▆▁▇▁▄▆▁▁▄▁▁▁▁▄▄▆▄▄▇▆▇▄▁▁▇▁▆▄▄▁▄▁▁▁▁▄ ▆
  19.9 ms Histogram: log(frequency) by time 25.9 ms <

 Memory estimate: 6.40 MiB, allocs estimate: 138409.

```

With the union type it is slower in this small test, but note the huge difference in the allocations.

---

<div class="post-metadata">

### Author: ![claforte](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/claforte/32/19115_2.png) [@claforte](https://discourse.julialang.org/u/claforte)
#### Post date: [December 14, 2021, 4:04am UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958/22 "2021-12-14T04:04:12Z")

</div>

FYI, I finished integrating @woclass’ changes and pushed them to github, in the proto.jl file. I plan to cut-and-paste them in the Pluto.jl notebook next week-end.

---

<div class="post-metadata">

### Author: ![claforte](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/claforte/32/19115_2.png) [@claforte](https://discourse.julialang.org/u/claforte)
#### Post date: [December 14, 2021, 8:19pm UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958/23 "2021-12-14T20:19:14Z")

</div>

Thanks DNF for bringing this up! I tried Xoroshiro, here’s my latest commit:

Use Xoroshiro128Plus RNG  
- large speed-up for small random functions…  
- small speed up in some small scenes  
- …but no noticeable difference in large scenes with high sample-count

… so the bottleneck appears to lie somewhere else (HitRecord allocations?)

Thanks anyway!

---

<div class="post-metadata">

### Author: ![lmiq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lmiq/32/18314_2.png) [@lmiq](https://discourse.julialang.org/u/lmiq)
#### Post date: [December 14, 2021, 9:09pm UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958/24 "2021-12-14T21:09:57Z")

</div>

> [@claforte](#):
>
> … so the bottleneck appears to lie somewhere else (HitRecord allocations?)

My quick test suggested that indeed the bottleneck is there.

Probably profiling is what is needed here. [It is easy to do that with the vscode extension.](https://discourse.julialang.org/t/julia-vs-code-extension-version-v0-17-released/42865).

---

<div class="post-metadata">

### Author: ![claforte](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/claforte/32/19115_2.png) [@claforte](https://discourse.julialang.org/u/claforte)
#### Post date: [December 15, 2021, 1:07am UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958/25 "2021-12-15T01:07:48Z")

</div>

My latest commit:

_4.35X speed-up using 16 threads_  
_- 20.16 minutes for the 1920x1080x1000 random\_spheres!_  
_- Easiest optimization ever… not even a whole line of code! 🙂_

I re-located the SIMD-optimized C++ implementation ([GitHub - GPSnoopy/RayTracingInOneWeekend: Implementation of Peter Shirley's Ray Tracing In One Weekend book.](https://github.com/GPSnoopy/RayTracingInOneWeekend)) and I was remembering incorrectly. GPSnoopy reports:

> The cover picture was rendered in about 1h running 16 threads on a Core i9-9900K @ 4.7GHz at 3840x2160 with 1024 samples per pixel. Clocking the CPU at 4.8GHz when running AVX code would cause it to reach over 90C, so be careful when running this code on an overclocked processor.

I tried to update my original description accordingly, but it looks like I don’t have the permission.

My tests were on 1920x1080 (i.e. 4 times less pixels), so my 20mins current record on a Ryzen2 is probably roughly equivalent to his 1 hour on his overpriced i9. 😉

I’ll try to run his code next, and hopefully their C++ compiler makes an attempt to be competitive on my AMD processor.

Stay tuned! 🙂

---

<div class="post-metadata">

### Author: ![paulmelis](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/paulmelis/32/35063_2.png) [@paulmelis](https://discourse.julialang.org/u/paulmelis)
#### Post date: [December 15, 2021, 8:53am UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958/26 "2021-12-15T08:53:55Z")

</div>

Just tried your Pluto notebook, really cool way to teach RT (and specifically the Raytracing in One Weekend series) like this! I do wonder how well the code edits and optimizations you’re doing above work in the Pluto notebook. Or are you doing those “offline”, run the tests, and only then check in the notebook again?

---

<div class="post-metadata">

### Author: ![claforte](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/claforte/32/19115_2.png) [@claforte](https://discourse.julialang.org/u/claforte)
#### Post date: [December 15, 2021, 3:02pm UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958/27 "2021-12-15T15:02:36Z")

</div>

> how well the code edits and optimizations you’re doing above work in the Pluto notebook. Or are you doing those “offline”, run the tests, and only then check in the notebook again?

I did all the initial implementation in Pluto.jl, I found it worked pretty well although it was getting long (and Pluto doesn’t allow us to cut-and-paste more than one cell at a time, which was painful to reorganize the code).

Now I’m using vscode to edit proto.jl so I can run @btime and get precise measurements. I checked long ago and this didn’t seem to work in Pluto.

Later today I plan to update the Pluto code with the massive (~12X) speed-ups from the past few days…

---

<div class="post-metadata">

### Author: ![sashmit](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/sashmit/32/18791_2.png) [@sashmit](https://discourse.julialang.org/u/sashmit)
#### Post date: [December 15, 2021, 6:01pm UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958/28 "2021-12-15T18:01:35Z")

</div>

i think pluto does allow you to copy/paste multiple cells. you just have to start a mouse drag outside of a cell and then move your mouse into the range of multiple cells, they should highlight to indicate that they are in the selection. then you can use your keyboard to do the usual clipboard commands (for me, it’s command-c and command-v on a mac).

---

<div class="post-metadata">

### Author: ![viralbshah](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/viralbshah/32/54_2.png) [@viralbshah](https://discourse.julialang.org/u/viralbshah)
#### Post date: [December 15, 2021, 6:45pm UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958/29 "2021-12-15T18:45:41Z")

</div>

I’ve been enjoying this thread. So many twists and turns. I feel like the journey from the starting code and all the changes that incrementally improve performance could be a great narrative would make for a really good youtube video.

Thinking out aloud, perhaps even good to have a session at JuliaCon for such narratives.

---

<div class="post-metadata">

### Author: ![joa-quim](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/joa-quim/32/227_2.png) [@joa-quim](https://discourse.julialang.org/u/joa-quim)
#### Post date: [December 15, 2021, 7:10pm UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958/30 "2021-12-15T19:10:49Z")

</div>

Funny, I was thinking in asking if at the end some summary could be made about the changes that lead to a ~12x speed-up.

I see that speed-up as good and not-so-good news. Good because, well, a 10 fold speed up is fantastic. Not-so-good because I feel I belong to those who think they have clever code but it turns out that deep tricks can make a huge difference.

---

<div class="post-metadata">

### Author: ![lmiq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lmiq/32/18314_2.png) [@lmiq](https://discourse.julialang.org/u/lmiq)
#### Post date: [December 15, 2021, 7:28pm UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958/31 "2021-12-15T19:28:29Z")

</div>

This is why all benchmarks comparing compiled languages end up being senseless when looked only from the point of view of the final performance. If some code is very optimized in one language, there is one effort that is necessary to achieve something reasonable (within a factor ~2). But the effort to get the last bit of performance can be huge, and worst, can be platform-dependent, test case dependent, etc.

That other thread about ray tracing was very curious in this aspect: low-level tricks were used in the Nim implementation and with that it beats all other languages. However, that was only for the exact same problem that was being benchmarked. If one changes somewhat the scene, the profiles change.

What I think is important in a study like this is go back and see what really made a difference. For example, inlining which function? Why? I have many times the experience that a lot of “code optimization” that I thought was being useful was actually just bad benchmarking, and a few important tips and changes make the greatest difference.

---

<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: [December 15, 2021, 7:49pm UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958/32 "2021-12-15T19:49:38Z")

</div>

Defining a global:

```julia
using RandomNumbers.Xorshifts
const _rng = Xoroshiro128Plus()

```

is almost certainly a bad idea in the multithreaded code.  
This is not thread safe, and will also hurt performance.

Also, disappointing that the TaskLocal rng in Julia 1.7 is slower than this, but `Random.Xoshiro` works well.

So I’d suggest allocating one `Xoroshiro128Plus()` or a `Random.Xoshiro` per thread, and passing it around.

---

<div class="post-metadata">

### Author: ![pxl-th](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/pxl-th/32/31939_2.png) [@pxl-th](https://discourse.julialang.org/u/pxl-th)
#### Post date: [December 15, 2021, 7:58pm UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958/33 "2021-12-15T19:58:04Z")

</div>

Hi!  
If you are curious, I implemented ray tracing some time ago in Julia as well: [GitHub - pxl-th/Trace.jl: Physically-based ray tracing on CPU](https://github.com/pxl-th/Trace.jl)  
Mostly following this excellent book: [Physically Based Rendering: From Theory to Implementation](https://www.pbr-book.org/3ed-2018/contents).

It also has multithreading support and BVH acceleration structure. I also spend some time trying to improve performance. Most of performance improvements I got was from the eliminations of allocations in ray → object intersections routines. But it still has a lot of stuff that can be improved, ideally I thought of writing some kind of custom allocator to remove all alocations, but I got lazy shortly after 🙂 .

It is able to simulate caustic effects using Stochastic Progressive Photon Mapping, though, which was my main goal (see image below).

 ![caustic-glass-sppm-100-iterations](https://global.discourse-cdn.com/julialang/original/3X/3/b/3bde343372d76cb3cf4bff06af50256ea54a9a46.png)

---

<div class="post-metadata">

### Author: ![paulmelis](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/paulmelis/32/35063_2.png) [@paulmelis](https://discourse.julialang.org/u/paulmelis)
#### Post date: [December 15, 2021, 10:00pm UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958/34 "2021-12-15T22:00:59Z")

</div>

> [@pxl-th](#):
>
> If you are curious, I implemented ray tracing some time ago in Julia as well: [GitHub - pxl-th/Trace.jl: Ray tracing](https://github.com/pxl-th/Trace.jl)  
> Mostly following this excellent book: [Physically Based Rendering: From Theory to Implementation](https://www.pbr-book.org/3ed-2018/contents).

Heh, I was wondering if porting PBRT to Julia was going to give cleaner (and fewer lines of) code, while still providing comparable performance. Impressive what you managed to achieve!

And since we’re posting ray-tracing-in-Julia projects, here’s another one (substantially less ambitious), which was an experiment of porting existing Python raytracing code (don’t ask) to Julia. Back then I wrote down details of the optimization journey.

> **[GitHub - paulmelis/sahara-julia: A port of a simple Python raytracer to...](https://github.com/paulmelis/sahara-julia)**
>
> A port of a simple Python raytracer to Julia, including some details on performance improvements - GitHub - paulmelis/sahara-julia: A port of a simple Python raytracer to Julia, including some deta...

---

<div class="post-metadata">

### Author: ![claforte](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/claforte/32/19115_2.png) [@claforte](https://discourse.julialang.org/u/claforte)
#### Post date: [December 15, 2021, 11:28pm UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958/35 "2021-12-15T23:28:07Z")

</div>

I’m up for a JuliaCon talk if others are interested. 🙂 I’m planning to present the results internally at AMD to hopefully rally more Julia supporters.

---

<div class="post-metadata">

### Author: ![claforte](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/claforte/32/19115_2.png) [@claforte](https://discourse.julialang.org/u/claforte)
#### Post date: [December 15, 2021, 11:29pm UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958/36 "2021-12-15T23:29:56Z")

</div>

> [@Elrod](#):
>
> So I’d suggest allocating one `Xoroshiro128Plus()` or a `Random.Xoshiro` per thread, and passing it around.

I’d like that as well, but I didn’t have time to research how to do it… if you happen to know, can you show me?

---

<div class="post-metadata">

### Author: ![claforte](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/claforte/32/19115_2.png) [@claforte](https://discourse.julialang.org/u/claforte)
#### Post date: [December 15, 2021, 11:44pm UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958/37 "2021-12-15T23:44:11Z")

</div>

IMHO just proving that, with some moderate level of expertise, it’s possible to match or exceed the performance of reasonably optimized C++ code, is a necessary step to convince C++ -centric organizations like my employer to take a closer look and back new projects that want to use Julia.

BTW, I now have more concrete comparison, with the C++ (GCC) and an ISPC implementation. The details are here:

[https://github.com/claforte/RayTracingWeekend.jl#competitive-targets-gpsnoopys-ispc-c-and-vulkan-implementations](https://github.com/claforte/RayTracingWeekend.jl#competitive-targets-gpsnoopys-ispc-c-and-vulkan-implementations)

The conclusion is:

This Julia implementation, with 14 threads, runs in **22m21s** , i.e. now a bit faster than the equivalent GCC C++ version! (22m59s)

However the ISPC version is incredibly fast in comparison: 6m5s.

I also briefly reviewed the code of each version, to confirm that we’re implementing the same thing, i.e. same max number of bounces, no BVH, etc.

I also tried to enable various Julia optimizer settings, `-O3 -ffast-math -march=znver2` and peppered @fastmath, @simd, @inbounds, and it made no difference. Memory allocations/traversal still seem to be the primary bottleneck.

---

<div class="post-metadata">

### Author: ![Oscar\_Smith](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/oscar_smith/32/25343_2.png) [@Oscar\_Smith](https://discourse.julialang.org/u/Oscar_Smith)
#### Post date: [December 15, 2021, 11:58pm UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958/38 "2021-12-15T23:58:24Z")

</div>

Any chance we can get a disassembled version of the ISPC assembly? I’d be curious to see what it does differently.

---

<div class="post-metadata">

### Author: ![claforte](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/claforte/32/19115_2.png) [@claforte](https://discourse.julialang.org/u/claforte)
#### Post date: [December 16, 2021, 12:12am UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958/39 "2021-12-16T00:12:58Z")

</div>

> [@Oscar\_Smith](#):
>
> Any chance we can get a disassembled version of the ISPC assembly?

I don’t know how, but you should be able compile yourself pretty easily: [GitHub - GPSnoopy/RayTracingInOneWeekend: Implementation of Peter Shirley's Ray Tracing In One Weekend book.](https://github.com/GPSnoopy/RayTracingInOneWeekend)

---

<div class="post-metadata">

### Author: ![lmiq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lmiq/32/18314_2.png) [@lmiq](https://discourse.julialang.org/u/lmiq)
#### Post date: [December 16, 2021, 12:27am UTC](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958/40 "2021-12-16T00:27:12Z")

</div>

Is it easy to test how the codes scale with the number of spheres?

These small tests may fall into the categories of problems that fall within a compiler heuristic and not the other to do union splitting of classes/types. Maybe this large difference are not sustained for more realistic problems.

[Previous page](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958.md?page=1)

[Next page](https://discourse.julialang.org/t/ray-tracing-in-a-week-end-julia-vs-simd-optimized-c/72958.md?page=3)
