производительность — Наивный перевод C ++ на Юлию, есть ли место для оптимизации?

Всякий раз, когда я сравниваю производительность Джулии с чудовищным C ++, я всегда поражаюсь. По своему опыту, я часто нахожу Джулию наравне с С ++ или даже быстрее. Мой последний эксперимент сравнивал скорость Джулии с Библиотека C ++ Armadillo используя свои микро тесты. В большинстве случаев я обнаружил, что Джулия быстрее, чем С ++, в таком числовом коде (осторожно: это мои собственные заявления, конечно же, YMMV).

Теперь я сделал наивный перевод этот код C ++ пересечения лучей и треугольников в Джулию и ожидал получить такие же результаты, как мои предыдущие эксперименты. Запустив оба кода, я получил следующий результат для C ++ (скомпилированный с помощью: gcc -std = c ++ 11 -lm -O3 -ffast-math):

Total intersection tests:  100,000,000
Hits:                      4,930,610 ( 4.93%)
Misses:                   95,069,390 (95.07%)

Total time:                       1.51 seconds
Millions of tests per second:    66.18

а для Юлии (версия 0.6.0 на Windows 10) я получил:

Total intersection tests:  100000000
Hits:             5221395  ( 5.221395%)
Misses:           94778605 (94.778605%)

Total time:         2.144723297 seconds
Millions of tests per second:   46.6260613384851

Ниже приведен построчный перевод на Юлию без каких-либо попыток оптимизации. Что не так с моим кодом? или Юлия struct менее эффективны? Где это 1.4X родом из? и как я могу оптимизировать этот код? Я знаю, что использую двойную точность, но это не проблема для Юлии.

struct vec3
x::Float64
y::Float64
z::Float64
end

mutable struct ray
orig::vec3
dir ::vec3
end

sub3(a::vec3, b::vec3) = vec3(a.x - b.x, a.y - b.y, a.z - b.z)
dot3(a::vec3, b::vec3) = a.x*b.x +  a.y*b.y + a.z*b.z
len(v::vec3) = sqrt( v.x * v.x + v.y * v.y + v.z * v.z  )
normalize(v) = begin l = length(v); vec3( v[1]/l, v[2]/l, v[3]/l ) end
cross3(a::vec3, b::vec3) = vec3(a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x)

# Ray-triangle intersection routine
function rayTriangleIntersect(r::ray, v0::vec3, v1::vec3, v2::vec3)::Float64
v0v1 = sub3(v1, v0);
v0v2 = sub3(v2, v0);
pvec = cross3(r.dir, v0v2);
detc = dot3(v0v1, pvec);

(detc < 0.000001) && return -Inf

invDet = 1.0 / detc;
tvec = sub3(r.orig, v0);
u = dot3(tvec, pvec) * invDet;

(u < 0 || u > 1) && return -Inf

qvec = cross3(tvec, v0v1);
v = dot3(r.dir, qvec) * invDet;

(v < 0 || u + v > 1) && return -Inf

return dot3( v0v2, qvec ) * invDet;
end

function randomSphere()::vec3
r1  = rand();
r2  = rand();
lat = acos(2r1 - 1) - pi/2;
lon = 2pi * r2;
return vec3( cos(lat)cos(lon), cos(lat)sin(lon), sin(lat) )
end

function generateRandomTriangles(numTriangles::Int64)#::Array{vec3}(3numTriangles)
vertices = Array{vec3}(3numTriangles)
for i = 1:3numTriangles
vertices[i] = randomSphere();
end
return vertices
end

function main_ray()
const NUM_RAYS = 1000;
const NUM_TRIANGLES = 100 * 1000;

vertices = generateRandomTriangles(NUM_TRIANGLES);
const numVertices = NUM_TRIANGLES * 3;

numHit  = 0;
numMiss = 0;

o  = vec3(0.0, 0.0, 0.0)
d  = vec3(0.0, 0.0, 0.0)
p1 = vec3(0.0, 0.0, 0.0)
r  = ray(o,d)

tTotal = @elapsed for i = 1:NUM_RAYS
r.orig = randomSphere();
p1     = randomSphere();
r.dir  = sub3(p1,r.orig);

for j = 0:div(numVertices,3) - 1
t = rayTriangleIntersect(r, vertices[j*3 + 1],
vertices[j*3 + 2],
vertices[j*3 + 3]);
t >= 0? numHit+=1 : numMiss+=1
end
end

numTests = NUM_RAYS * NUM_TRIANGLES;
hitPerc  = numHit / numTests * 100.0
missPerc = numMiss / numTests * 100.0
mTestsPerSecond = numTests / tTotal / 10^6

println("Total intersection tests:  $numTests");
println("  Hits:\t\t\t    $numHit  ( $hitPerc%)");
println("  Misses:\t\t    $numMiss ($missPerc%)\n");
println("Total time:\t\t\t$tTotal seconds");
println("Millions of tests per second:\t$mTestsPerSecond\n");
end

main_ray();

2

Решение

Это тот код, который действительно выигрывает от запуска julia с -O3, и при этом вы также можете добавить —math-mode = fast.
При этом время на моей машине меняется с 8,6 до 3,2 с.
Я также использую Float32 как в коде C ++, так как это может иметь значение здесь.
Добавление @inline к пересечению треугольника также увеличивает скорость (теперь 1,9 с), думаю, это может лучше оптимизировать тело цикла for, чем!

Вот немного по-другому структурированный код:


используя StaticArrays
const vec3 = SVector {3, Float32}
struct ray
ориг :: vec3
dir :: vec3
конец

# Процедура пересечения лучей и треугольников
@ встроенная функция rayTriangleIntersect (r :: ray, v0 :: vec3, v1 :: vec3, v2 :: vec3) :: Float64
v0v1 = v1 .- v0
v0v2 = v2 .- v0
pvec = cross (r.dir, v0v2);
detc = точка (v0v1, pvec);

(Детек 1)  return -Inf32

qvec = cross (tvec, v0v1);
v = точка (r.dir, qvec) * invDet;

(v 1)  return -Inf32

возвращаемая точка (v0v2, qvec) * invDet;
конец

функция randomSphere () :: vec3
r1 = ранд (Float32);
r2 = ранд (Float32);
lat = acos (2r1 - 1) - pi / 2f0;
lon = 2f0 * pi * r2;
return vec3 (cos (lat) cos (lon), cos (lat) sin (lon), sin (lat))
конец

функция generateRandomTriangles (numTriangles :: Int64) # :: Array {vec3} (3numTriangles)
vertices = Array {vec3} (3numTriangles)
для i = 1: 3numTriangles
вершины [i] = randomSphere ();
конец
вернуть вершины
конец

функция main_ray (вершины)
o = vec3 (0,0, 0,0, 0,0)
d = vec3 (0,0, 0,0, 0,0)
p1 = vec3 (0,0, 0,0, 0,0)
r = ray (o, d)
numHit = 0;
numVertices = длина (вершины)
для i = 1: 1000
p1 = randomSphere ();
r = ray (
randomSphere (),
(p1 .- r.orig)
)

@simd для j = 0: div (numVertices, 3) - 1
@inbounds t = rayTriangleIntersect (r, вершины [j * 3 + 1],
вершины [j * 3 + 2],
вершины [j * 3 + 3]);
numHit + = ifelse (t> = 0, 1, 0)
конец
конец
конец

NUM_TRIANGLES = 100 * 1000;

вершины = generateRandomTriangles (NUM_TRIANGLES);

используя BenchmarkTools
b = @benchmark main_ray (вершины)
Println (б)
0

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]