Увеличивает ли умножение целочисленного массива на число с плавающей точкой производительность по сравнению с генерацией массива с плавающей точкой в ​​Юлии? - PullRequest
4 голосов
/ 11 марта 2020

Мне было интересно, является ли выражение dx*collect(0:J), где J равно Int64, а dx равно Float64, вычислительно более эффективно, чем collect(0:dx:dx*J), или наоборот?

Чтобы сформулировать это иначе: эффективнее ли создавать целочисленный массив и умножать (J + 1) раз на число с плавающей точкой (1-й случай) или генерировать массив с плавающей точкой в ​​первую очередь (2-й случай)?

Каковы преимущества / недостатки каждого случая?

Ответы [ 2 ]

3 голосов
/ 11 марта 2020

Я уверен, что найдутся люди, которые могут дать вам полное объяснение в отношении того, какой код генерируется LLVM et c., Но в простых случаях, таких как этот, когда вы сомневаетесь, вы можете просто провести сравнительный анализ :

julia> using BenchmarkTools

julia> collect_first(dx, J) = dx*collect(0:J)
collect_first (generic function with 1 method)

julia> collect_all(dx, J) = collect(0:dx:dx*J)
collect_all (generic function with 1 method)

julia> @btime collect_first(3.2, 100);
  172.194 ns (2 allocations: 1.75 KiB)

julia> @btime collect_all(3.2, 100);
  359.330 ns (1 allocation: 896 bytes)

julia> @btime collect_first(3.2, 10_000);
  11.300 μs (4 allocations: 156.53 KiB)

julia> @btime collect_all(3.2, 10_000);
  18.601 μs (2 allocations: 78.27 KiB)

julia> @btime collect_first(3.2, 100_000);
  145.499 μs (4 allocations: 1.53 MiB)

julia> @btime collect_all(3.2, 100_000);
  183.300 μs (2 allocations: 781.39 KiB)

julia> @btime collect_first(3.2, 1_000_000);
  5.650 ms (4 allocations: 15.26 MiB)

julia> @btime collect_all(3.2, 1_000_000);
  3.806 ms (2 allocations: 7.63 MiB)

Таким образом, если вы сначала собираете, вы удваиваете количество выделений (предположительно, когда происходит распределение при collect, а затем снова при умножении для получения результата), однако для небольших массивов кажется, что это все еще быстрее с точки зрения времени вычислений. Для сбора больших массивов после умножения диапазон строго доминирует.

2 голосов
/ 12 марта 2020

Обратите внимание, что оба решения не дают одинаковых результатов:

julia> dx = 0.1;
julia> J = 10;

julia> a = dx*collect(0:J)
11-element Array{Float64,1}:
 0.0                
 0.1                
 0.2                
 0.30000000000000004
 0.4                
 0.5                
 0.6000000000000001 
 0.7000000000000001 
 0.8                
 0.9                
 1.0                

julia> b = collect(0:dx:J*dx)
11-element Array{Float64,1}:
 0.0
 0.1
 0.2
 0.3
 0.4
 0.5
 0.6
 0.7
 0.8
 0.9
 1.0

julia> maximum(abs.(b-a))
1.1102230246251565e-16

Это связано с хитрыми проблемами с плавающей запятой. Как выясняется, в типах диапазонов с плавающей точкой, таких как 0:dx:J*dx (см. Ниже), реализация таких диапазонов идет очень долго, пытаясь избежать ошибок округления, удваивая точность значений с плавающей запятой для некоторых промежуточных результатов и их результатов. немного отличаются от простой реализации на основе целых чисел.

julia> typeof(0:dx:J*dx)
StepRangeLen{Float64,Base.TwicePrecision{Float64},Base.TwicePrecision{Float64}}

Также обратите внимание, что понимание может дать вам те же результаты, что и ваша первая реализация, без какого-либо промежуточного распределения:

julia> c = [dx*i for i in 0:J]
11-element Array{Float64,1}:
 0.0                
 0.1                
 0.2                
 0.30000000000000004
 0.4                
 0.5                
 0.6000000000000001 
 0.7000000000000001 
 0.8                
 0.9                
 1.0                

julia> maximum(abs.(c-a))
0.0

Тесты, по-видимому, указывают на то, что при производительности и игнорировании возможных проблем округления использование понимания в большинстве случаев быстрее:

julia> using BenchmarkTools
julia> collect_first(dx, J) = dx*collect(0:J);
julia> collect_all(  dx, J) = collect(0:dx:dx*J);
julia> comprehension(dx, J) = [dx*i for i in 0:J];

# Small size
julia> @btime collect_first(0.1, 100);
  206.895 ns (2 allocations: 1.75 KiB)

julia> @btime collect_all(0.1, 100);
  476.533 ns (1 allocation: 896 bytes)

julia> @btime comprehension(0.1, 100);
  134.363 ns (1 allocation: 896 bytes)

# Large size
julia> @btime collect_first(0.1, 1_000_000);
  1.970 ms (4 allocations: 15.26 MiB)

julia> @btime collect_all(0.1, 1_000_000);
  2.557 ms (2 allocations: 7.63 MiB)

julia> @btime comprehension(0.1, 1_000_000);
  900.449 μs (2 allocations: 7.63 MiB)
...