В Юлии стоит сузить тип словаря, возвращаемого `JSON.parsefile` - PullRequest
0 голосов
/ 15 февраля 2019

Я пишу код Джулии, чьи входные данные являются файлами json, который выполняет анализ в (область математических финансов) и записывает результаты как json.Код является портом от R. В надежде на улучшение производительности.

Я анализирую входные файлы, используя JSON.parsefile.Это возвращает Dict, в котором я наблюдаю, что все векторы имеют тип Array{Any,1}.Так получилось, что я знаю, что входной файл никогда не будет содержать векторы смешанного типа, такие как некоторые String с и некоторые Number с.Поэтому я написал следующий код, который, кажется, работает хорошо и является «безопасным» в том смысле, что если вызовы convert завершаются неудачно, вектор продолжает иметь тип Array{Any,1}.

function typenarrow!(d::Dict)
    for k in keys(d)
        if d[k] isa Array{Any,1}
            d[k] = typenarrow(d[k])
        elseif d[k] isa Dict
            typenarrow!(d[k])
        end
    end
end

function typenarrow(v::Array{Any,1})
    for T in [String,Int64,Float64,Bool,Vector{Float64}]
        try
            return(convert(Vector{T},v))
        catch; end        
    end
    return(v)
end

Myвопрос: стоит ли это делать?Могу ли я ожидать, что код, который обрабатывает содержимое Dict, будет выполняться быстрее, если я сужу этот тип?Я думаю, что ответ «да» в том, что Советы по производительности Julia рекомендуют «Аннотировать значения, взятые из нетипизированных местоположений», и этот подход гарантирует отсутствие «нетипизированных местоположений».

1 Ответ

0 голосов
/ 15 февраля 2019

Существует два уровня ответа на этот вопрос:

Уровень 1

Да, это поможет производительности кода.Смотрите, например, следующий тест:

julia> using BenchmarkTools

julia> x = Any[1 for i in 1:10^6];

julia> y = [1 for i in 1:10^6];

julia> @btime sum($x)
  26.507 ms (477759 allocations: 7.29 MiB)
1000000

julia> @btime sum($y)
  226.184 μs (0 allocations: 0 bytes)
1000000

Вы можете написать свою typenarrow функцию, используя немного более простой подход, подобный следующему:

typenarrow(x) = [v for v in x]

, так как использование понимания даст векторконкретный тип (при условии, что ваш исходный вектор однороден)

Уровень 2

Это не совсем оптимально.Проблема, которая все еще остается, состоит в том, что у вас есть Dict, который является контейнером с параметром абстрактного типа (см. https://docs.julialang.org/en/latest/manual/performance-tips/#Avoid-containers-with-abstract-type-parameters-1). Поэтому, чтобы вычисления были быстрыми, вы должны использовать барьерную функцию (см. * 1023).*https://docs.julialang.org/en/latest/manual/performance-tips/#kernel-functions-1) или используйте аннотацию типа для введенных вами переменных (см. https://docs.julialang.org/en/v1/manual/types/index.html#Type-Declarations-1).

В идеальном мире ваш Dict будет иметь ключи и значения однородных типов, и тогда все будет максимально быстро, но если яправильно понимать ваш код значения в вашем случае не являются однородными.

РЕДАКТИРОВАТЬ

Чтобы решить проблему уровня 2, вы можете преобразовать Dict в NamedTuple какэто (это минимальный пример, предполагающий, что Dict s вкладывается только в Dict s напрямую, но его должно быть достаточно легко расширить, если вы хотите большей гибкости).

Во-первых, функция, выполняющая преобразованиевыглядит так:

function typenarrow!(d::Dict)
    for k in keys(d)
        if d[k] isa Array{Any,1}
            d[k] = [v for v in d[k]]
        elseif d[k] isa Dict
            d[k] = typenarrow!(d[k])
        end
    end
    NamedTuple{Tuple(Symbol.(keys(d)))}(values(d))
end

Теперь MWE его использования:

julia> using JSON

julia> x = """
       {
         "name": "John",
         "age": 27,
         "values": {
           "v1": [1,2,3],
           "v2": [1.5,2.5,3.5]
         },
         "v3": [1,2,3]
       }
       """;

julia> j1 = JSON.parse(x)
Dict{String,Any} with 4 entries:
  "name"   => "John"
  "values" => Dict{String,Any}("v2"=>Any[1.5, 2.5, 3.5],"v1"=>Any[1, 2, 3])
  "age"    => 27
  "v3"     => Any[1, 2, 3]

julia> j2 = typenarrow!(j1)
(name = "John", values = (v2 = [1.5, 2.5, 3.5], v1 = [1, 2, 3]), age = 27, v3 = [1, 2, 3])

julia> dump(j2)
NamedTuple{(:name, :values, :age, :v3),Tuple{String,NamedTuple{(:v2, :v1),Tuple{Array{Float64,1},Array{Int64,1}}},Int64,Array{Int64,1}}}
  name: String "John"
  values: NamedTuple{(:v2, :v1),Tuple{Array{Float64,1},Array{Int64,1}}}
    v2: Array{Float64}((3,)) [1.5, 2.5, 3.5]
    v1: Array{Int64}((3,)) [1, 2, 3]
  age: Int64 27
  v3: Array{Int64}((3,)) [1, 2, 3]

Прелесть этого подхода в том, что Джулия будет знать все типы в j2, поэтому, если вы передадите j2 долюбая функция в качестве параметра, все вычисления внутри этой функции будут быстрыми.

Недостатком этого подхода является то, что функция, принимающая j2, должна быть предварительно скомпилирована, что может быть проблематично, если структура j2огромный (так как тогда структура полученного NamedTuple является сложной), а объем работы, выполняемой вашей функцией, относительно невелик.Но для небольших JSON-ов (небольших в смысле структуры, поскольку содержащиеся в них векторы могут быть большими - их размер не добавляет сложности), этот подход оказался эффективным в нескольких разработанных мною приложениях.

...