Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

constructors and functions behave differently with respect to isbitstype, sizeof, etc. #38716

Closed
goretkin opened this issue Dec 5, 2020 · 7 comments

Comments

@goretkin
Copy link
Contributor

goretkin commented Dec 5, 2020

For one use case, I am thinking of Int64 and round as functions:

julia> Int64(3)
3

julia> round(3)
3

and I want to represent their types respectively. Unfortunately

julia> isbitstype(typeof(round))
true

julia> isbitstype(Type{Int64})
false

And more pertinent to my problem:

julia> struct Foo{T}
       _::T
       end

julia> Foo(round)
Foo{typeof(round)}(round)

julia> sizeof(Foo(round))
0

julia> Foo(Int64)
Foo{DataType}(Int64)

julia> sizeof(Foo(Int64))
8

julia> Foo{Type{Int64}}(Int64)
Foo{Type{Int64}}(Int64)

julia> sizeof(Foo{Type{Int64}}(Int64))
8

If I understand correctly, if T in Foo{T} is a singleton type, that is there is only one value x::T, then sizeof(Foo{T}) can in principle be 0. There is only one x::typeof(round) and only one x::Type{Int64}.

(reference: https://julialang.zulipchat.com/#narrow/stream/225542-helpdesk/topic/size.20of.20tuple.20.2F.20struct.20of.20singleton.20type)

@vtjnash
Copy link
Sponsor Member

vtjnash commented Dec 5, 2020

I think it's already explained in that zulipchat that Type{T} isn't a singleton, and closure Functions don't work differently from this

@vtjnash vtjnash closed this as completed Dec 5, 2020
@goretkin
Copy link
Contributor Author

goretkin commented Dec 5, 2020

#36591 helped address some of the confusion, and even though not all Type{...} are singleton (e.g. isa(Int, Type{<:Number})), I still do not understand why Type{Int} is not formally a singleton, though morally it certainly seems to be:

https://github.com/JuliaLang/julia/blob/65382c708d1fafeebb7b896dfeb2795362fe8921/doc/src/manual/types.md#typet-type-selectorsid-man-typet-type

For each type T, Type{T} is an abstract parametric type whose only instance is the
object T.

and closure Functions don't work differently from this

Can you explain why that is a justification for the current behavior? For closures

julia> function foo(x)
           g() = x
       end
foo (generic function with 1 method)

julia> typeof(foo(0))
var"#g#1"{Int64}

julia> T = typeof(foo(1))
var"#g#1"{Int64}

julia> isa(foo(0), T)
true

julia> isa(foo(1), T)
true

julia> foo(0) === foo(1)
false

T is clearly not a singleton. And also intuitively, the closure variable x needs to go somewhere.

In what sense is Int64 a closure? And in what sense, aside from

julia> Core.Compiler.issingletontype(Type{Int64})
false

julia> Base.issingletontype(Type{Int64})
false

is it not a singleton? Are there arguments that satisfy the following function:

function test(a, b)
    a !== b && isa(a, Type{Int64}) && isa(b, Type{Int64})
end

@vtjnash
Copy link
Sponsor Member

vtjnash commented Dec 5, 2020

Int64 is the type of a closure, because it holds data. In that same sense, Type{Int64} is also, as it captures T from the scope (values can be captured in multiple ways).

Yes, in the type domain, those are inhabited by Union{} also. That's not relevant for a function call (as there are no instances of that type), but can be relevant for a field, since those can hold the value of type Union{}. The captured value could typically be stored more compactly, but that's not quite the same as being an actual singleton, as it's instead implemented internally by special arrangement.

@goretkin
Copy link
Contributor Author

goretkin commented Dec 5, 2020

It is still not clear how to reconcile:

I think it's already explained in that zulipchat that Type{T} isn't a singleton

julia/base/docs/basedocs.jl

Lines 1165 to 1170 in 65382c7

"""
Core.Type{T}
`Core.Type` is an abstract type which has all type objects as its instances.
The only instance of the singleton type `Core.Type{T}` is the object
`T`.

Type{Int64} is also [the type of a closure], as it captures T from the scope (values can be captured in multiple ways).

I understand that Type is unique, and so am I right to understand that that does not apply to e.g. Foo{T}? What are the consequences of making Type not unique in this specific way?

Yes, in the type domain, those are inhabited by Union{} also.

Sorry, I can't figure out what "those" refers to.

More concretely, I hope this chunk helps me communicate what I want, and why I think it's reasonable, and helps others communicate why not. I compare

  1. a "type"
  2. a for-real closure
  3. a for-real function

to try to discover what's the rub.

function foo(x)
    g(y) = x + y
end

struct MyInt64
    _::Int64
end

things = [MyInt64, foo(0), identity]

println("\n", "all can be called:")
@show things[1](789)
@show things[2](789)
@show things[3](789)

println("\n", "1 and 2 hold data")
@show sizeof(things[1])
@show sizeof(things[2])
@show sizeof(things[3])

println("\n", "1, 2, 3 are all different types:")
@show typeof(things[1])
@show typeof(things[2])
@show typeof(things[3])

println("\n", "1 is unique in relation to `Type{thing}`")
for x in [:(things[1]), :(things[2]), :(things[3])]
    eval(:(@show isa($x, typeof($x))))
    eval(:(@show isa($x, Type{$x})))
end

# Define `my_typeof` that also satisfies `isa(x, my_typeof(x))`
my_typeof(T) = typeof(T)
my_typeof(T::DataType) = Type{T}

println("\n", "Double-check `my_typeof`")
for x in [:(things[1]), :(things[2]), :(things[3])]
    eval(:(@show isa($x, my_typeof($x))))  
end

println("\n", "typeof singleton status:")
@show Base.issingletontype(typeof(things[1]))
@show Base.issingletontype(typeof(things[2]))
@show Base.issingletontype(typeof(things[3]))

println("\n", "my_typeof singleton status (no change):")
@show Base.issingletontype(my_typeof(things[1]))
@show Base.issingletontype(my_typeof(things[2]))
@show Base.issingletontype(my_typeof(things[3]))

struct Empty{T}
end

make_Empty_direct(T) = Empty{T}()
make_Empty_my(x) = make_Empty_direct(my_typeof(x))

my_issingletontype(x) = Base.issingletontype(x)
my_issingletontype(::Type{Type{T}}) where {T} = Base.isconcretetype(T)

println("\n", "intuitive predicate for singleton. 1 and 3 are the same")
for x in [:(things[1]), :(things[2]), :(things[3])]
    eval(:(@show my_issingletontype(my_typeof($x))))  
end

# if there is a way to get from `typeof(identity)` to `identity`, that would allow a better definition for `get_thing`.
println("\n", "Define get_thing")
for thing in things
    !my_issingletontype(my_typeof(thing)) && continue
    T = Empty{my_typeof(thing)}
    println("\ton $T")
    @eval get_thing(::$(T), args...) = $(thing)
end

call_thing(e::Empty, args...) = get_thing(e)

println("\n", "1 and 3 are the same, and 1 and 2 are different.")
@show call_thing(make_Empty_my(things[1]), 789)
# @show call_thing(make_Empty_my(things[2]), 789) # this cannot mean anything.
@show call_thing(make_Empty_my(things[3]), 789)

# Since that feat is possible, then this certainly is:
struct Wrap{T}
    _::T
end

make_Wrap_my(x) = Wrap{my_typeof(x)}(x)
get_callable(w::Wrap) = w._
get_callable(w::Wrap) = w._
call_thing(thing::Wrap, args...) = get_callable(thing)(args...)

println("\n", "this works for all three:")
@show call_thing.(make_Wrap_my.(things), 789)
println("\n", "and so in principle `sizeof(make_Wrap_my(Int64))` can be zero, but:")
@show sizeof.(make_Wrap_my.(things))
nothing

output

all can be called:
(things[1])(789) = MyInt64(789)
(things[2])(789) = 789
(things[3])(789) = 789

1 and 2 hold data
sizeof(things[1]) = 8
sizeof(things[2]) = 8
sizeof(things[3]) = 0

1, 2, 3 are all different types:
typeof(things[1]) = DataType
typeof(things[2]) = var"#g#13"{Int64}
typeof(things[3]) = typeof(identity)

1 is unique in relation to `Type{thing}`
things[1] isa typeof(things[1]) = true
things[1] isa Type{things[1]} = true
things[2] isa typeof(things[2]) = true
things[2] isa Type{things[2]} = false
things[3] isa typeof(things[3]) = true
things[3] isa Type{things[3]} = false

Double-check `my_typeof`
things[1] isa my_typeof(things[1]) = true
things[2] isa my_typeof(things[2]) = true
things[3] isa my_typeof(things[3]) = true

typeof singleton status:
Base.issingletontype(typeof(things[1])) = false
Base.issingletontype(typeof(things[2])) = false
Base.issingletontype(typeof(things[3])) = true

my_typeof singleton status (no change):
Base.issingletontype(my_typeof(things[1])) = false
Base.issingletontype(my_typeof(things[2])) = false
Base.issingletontype(my_typeof(things[3])) = true

intuitive predicate for singleton. 1 and 3 are the same
my_issingletontype(my_typeof(things[1])) = true
my_issingletontype(my_typeof(things[2])) = false
my_issingletontype(my_typeof(things[3])) = true

Define get_thing
        on Empty{Type{MyInt64}}
        on Empty{typeof(identity)}

1 and 3 are the same, and 1 and 2 are different.
call_thing(make_Empty_my(things[1]), 789) = MyInt64
call_thing(make_Empty_my(things[3]), 789) = identity

this works for all three:
call_thing.(make_Wrap_my.(things), 789) = Any[MyInt64(789), 789, 789]

and so in principle `sizeof(make_Wrap_my(Int64))` can be zero, but:
sizeof.(make_Wrap_my.(things)) = [8, 8, 0]

Also to emphasize the point things[2] "actually" holds data:

julia> reinterpret(Int64, [things[2]])
1-element reinterpret(Int64, ::Array{var"#g#13"{Int64},1}):
 0

and the others do not.

@goretkin
Copy link
Contributor Author

goretkin commented Dec 7, 2020

And as a better concrete example of how this behavior affects the memory allocation of broadcasting machinery right now:

using BenchmarkTools

function foo(fs, n)
    x = 0
    for i = 1:n
        bc = Base.broadcasted(fs[mod1(i, end)], 1:i)
        x += sum(bc)
    end
    return x
end
julia> @benchmark foo([x->x, identity], 1000)
BenchmarkTools.Trial:
  memory estimate:  124.42 KiB
  allocs estimate:  4958
  --------------
  minimum time:     718.429 μs (0.00% GC)
  median time:      751.449 μs (0.00% GC)
  mean time:        793.813 μs (0.32% GC)
  maximum time:     2.212 ms (61.57% GC)
  --------------
  samples:          6293
  evals/sample:     1

julia> @benchmark foo([x->x, Int], 1000)
BenchmarkTools.Trial:
  memory estimate:  140.08 KiB
  allocs estimate:  5459
  --------------
  minimum time:     703.243 μs (0.00% GC)
  median time:      785.698 μs (0.00% GC)
  mean time:        800.921 μs (0.41% GC)
  maximum time:     2.364 ms (52.07% GC)
  --------------
  samples:          6236
  evals/sample:     1

which essentially boils down to

julia> sizeof(Broadcast.broadcasted(identity, 1:10))
16

julia> sizeof(Broadcast.broadcasted(Int, 1:10))
24

@vtjnash
Copy link
Sponsor Member

vtjnash commented Dec 8, 2020

Okay, but that seems to show that it's faster (though likely it's about the same speed), so it doesn't show why to change this. There is an optimization predicate (Core.Compiler.hasuniquerep) that already does classify these in some places.

@goretkin
Copy link
Contributor Author

goretkin commented Dec 8, 2020

I should not have included the times---the focus is on the memory estimate. And really what I care about is the storage as shown by the sizeof, so even the memory allocation reported by @benchmark is a proxy.

The bottom line is that in FixArgs.jl there is a struct Fix that is more or less analogous to Broadcast.Broadcasted, in that it has a reference to a callable thing. Unlike Broadcast.Broadcasted, I anticipate having arrays of these Fix types, and so I do care about the storage associated with it.

I hope that helps clarify why this is an issue for me. @mbauman helped me see that I wasn't clear on this, and I intend to write a summary of the conversation we had on slack, and that may help clarify further.

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

No branches or pull requests

2 participants