Skip to content

Conversation

@tkf
Copy link
Collaborator

@tkf tkf commented Aug 14, 2018

This is alternative to #39. If we want to handle mutable arrays (such as Array) by always copying them, this PR provides an implementation for it. This is consistent with how nomut branch handles mutable struct.

@tkf tkf mentioned this pull request Aug 14, 2018
@codecov-io
Copy link

codecov-io commented Aug 14, 2018

Codecov Report

Merging #40 into nomut will increase coverage by 0.11%.
The diff coverage is 100%.

Impacted file tree graph

@@            Coverage Diff            @@
##           nomut      #40      +/-   ##
=========================================
+ Coverage   97.5%   97.61%   +0.11%     
=========================================
  Files          4        4              
  Lines        160      168       +8     
=========================================
+ Hits         156      164       +8     
  Misses         4        4
Impacted Files Coverage Δ
src/lens.jl 100% <100%> (ø) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d8a8730...05179c8. Read the comment docs.

export set, get, modify

import Base: get
using Base: setindex, getproperty
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed setindex since we should be using our internal function _setindex everywhere and avoid future bugs due to typo.

set(l::IndexLens, obj, val) = _setindex(obj, val, l.indices...)

@generated function _setindex(obj, val, indices...)
if hasmethod(Base.setindex, Tuple{obj, val, indices...})
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know hasmethod is not pure and therefore is not legit inside @generated in principle. But I've been seeing code deviating from the principle and somehow magically working. My guess was that this implementation is OK as long as _setindex is called after Base.setindex(obj, ...) is defined. Since our StaticArrays tests are passing, this implementation is fine?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you show a place, where people use code like this? Generally such things can cause bugs depending on import order of packages. But I don't understand enough to judge how safe this case is. I would guess that you could break this by

using Setfield
using StaticArrays

# define your own StaticArray with custom setindex

OTOH if you don't make this a generated function, with the new compiler it might still perform very well. Have you tested?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I don't know the working code base using exactly this. I should test this and also do benchmark for the dynamic version.

BTW, what is the new compiler optimization you are talking about? The Union-splitting https://julialang.org/blog/2018/08/union-splitting sounds very close but the branching information still has to sit at the type level, right? Also, this blog post is mentioning that method lookup is still slow.

But even if @generated approach does not work, we can always have a dynamic version as a fallback and then manually implement fast non-branching version for popular types (like Array, Tuple and SArray). Probably it is not hard to get > 90% coverage by implementing it for a few types.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had better constant propagation in mind. And I thought hasmethod might compile down to true or false, but it does not. It does dynamically look if there is a method.

@tkf
Copy link
Collaborator Author

tkf commented Aug 15, 2018

So my benchmark shows the static version is ~20 times faster.

@generated function setindex_dynamic(obj, val, indices...)
    quote
        if hasmethod(Base.setindex, $(Tuple{obj, val, indices...}))
            setter = Base.setindex
        else
            setter = setindex_on_copy
        end
        setter(obj, val, indices...)
    end
end

@generated function setindex_static(obj, val, indices...)
    if hasmethod(Base.setindex, Tuple{obj, val, indices...})
        setter = Base.setindex
    else
        setter = setindex_on_copy
    end
    quote
        $setter(obj, val, indices...)
    end
end

function setindex_on_copy(obj, val, indices...)
    clone = similar(obj, promote_type(eltype(obj), typeof(val)))
    copyto!(clone, obj)
    setindex!(clone, val, indices...)
    return clone
end


using BenchmarkTools
a = [1, 2, 3]
println("\n", "*** setindex_dynamic")
display(@benchmark setindex_dynamic($a, 1, 1))
println("\n", "*** setindex_static")
display(@benchmark setindex_static($a, 1, 1))

using InteractiveUtils
versioninfo()

Output:

*** setindex_dynamic
BenchmarkTools.Trial:
  memory estimate:  144 bytes
  allocs estimate:  2
  --------------
  minimum time:     473.071 ns (0.00% GC)
  median time:      476.987 ns (0.00% GC)
  mean time:        529.616 ns (5.03% GC)
  maximum time:     177.103 μs (99.55% GC)
  --------------
  samples:          10000
  evals/sample:     196
*** setindex_static
BenchmarkTools.Trial:
  memory estimate:  112 bytes
  allocs estimate:  1
  --------------
  minimum time:     25.980 ns (0.00% GC)
  median time:      28.185 ns (0.00% GC)
  mean time:        38.229 ns (17.35% GC)
  maximum time:     38.632 μs (99.82% GC)
  --------------
  samples:          10000
  evals/sample:     992Julia Version 1.0.0
Commit 5d4eaca0c9 (2018-08-08 20:58 UTC)
Platform Info:
  OS: Linux (x86_64-pc-linux-gnu)
  CPU: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.0 (ORCJIT, skylake)

@tkf
Copy link
Collaborator Author

tkf commented Aug 15, 2018

I tried to invoke the bug in the hasmethod hack but the only way I can invoke the bug is to write somewhat unrealistic code (see struct B below). Here is what I tried.

module TestHasMethodHack

using Test

function setindex_on_copy(obj, val, indices...)
    clone = similar(obj, promote_type(eltype(obj), typeof(val)))
    copyto!(clone, obj)
    setindex!(clone, val, indices...)
    return clone
end


struct A
    x
end

Base.setindex(::A, x, ::Integer) = A(x)


@generated function setindex_static(obj, val, indices...)
    if hasmethod(Base.setindex, Tuple{obj, val, indices...})
        setter = Base.setindex
    else
        setter = setindex_on_copy
    end
    quote
        $setter(obj, val, indices...)
    end
end

@assert setindex_static([0], 1, 1) == [1]  # Having it here (or commenting it out) does not change anything.


struct B
    x
end

try
    setindex_static(B(1), 2, 1)
catch err
    @info "Ignoring" exception=err
end

Base.setindex(::B, x, ::Integer) = B(x)


struct C
    x
end

Base.setindex(::C, x, ::Integer) = C(x)


@testset begin
    @test setindex_static(A(0), 1, 1) == A(1)
    @test_broken setindex_static(B(0), 1, 1) == B(1)
    @test setindex_static(C(0), 1, 1) == C(1)
end

end  # module

Output:

┌ Info: Ignoring
│   exception =
│    MethodError: no method matching similar(::Main.TestHasMethodHack.B, ::Type{Any})
│    Closest candidates are:
│      similar(::Array{T,1}, ::Type) where T at array.jl:329
│      similar(::Array{T,2}, ::Type) where T at array.jl:330
│      similar(::Array, ::Type, ::Tuple{Vararg{Int64,N}}) where N at array.jl:332
└      ...
Test Summary: | Pass  Broken  Total
test set      |    2       1      3

@jw3126
Copy link
Owner

jw3126 commented Aug 15, 2018

Thanks for the example, I agree it is somewhat exotic. But this kind of thing comes up in REPL sessions. Is setindex on Vector important to you? I would prefer removing it over having a slow / slightly buggy version. But if it is important to you, we can support it.

@tkf
Copy link
Collaborator Author

tkf commented Aug 15, 2018

Is setindex on Vector important to you?

Nope :) It is just me trying to make test_quicktypes.jl pass.

I would prefer removing it over having a slow / slightly buggy version.

Totally agree. Let's pull #39 then!

@tkf tkf closed this Aug 15, 2018
@tkf
Copy link
Collaborator Author

tkf commented Aug 18, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants