-
Notifications
You must be signed in to change notification settings - Fork 264
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
Null pointers #584
Comments
Wouldn't it be better to implement something like Rust's null pointer optimization for strict (unlifted?) datatypes? That way using a maybe for all elements would not incur in a performance penalty without adding (more) footguns |
null pointers are very useful for something else: adding a distinguished value to each element of a container. The reason they're a mistake is you can't stop the compiler/code/machine allocating an address that happens to coincide with (one of) your distinguished value(s). So your 'magic' would need to look something like:
I appreciate the likelihood of an actual clash is vanishing small -- which is why null pointers work in practice. But Haskell is supposed to come with memory-safety guarantees, which your |
@AntC2 , I don't know what you're talking about. My nulls are addresses of nullary data constructors, which GHC already allocates statically. There are definitely safely concerns with my hacked up implementation, but that isn't one of them. |
That optimization sounds nice, but it seems to be limited to certain fairly monomorphic contexts. I don't see how to apply it to anything polymorphic like |
My nulls are addresses of nullary data constructors, ... Then I suggest calling them 'Null pointers' is completely misleading -- especially in the same breath as Hoare's "billion dollar mistake". In re-reading your post, I don't know what you're talking about. Specifically in
|
Yes, the implementation could totally change to mess that up. That's part of why I want proper support for something of similar power.
As above.
Because
Unboxed arrays are available. If we have something like |
I meant something like Then what are the use cases for |
The problem is more with multiple non-niladic constructors, or more-than-unary constructors:
So now we need an explicit constructor to tell which case; or to tell what memory footprint. Then your use case is only |
The representation I was suggesting was an unboxed array of
I think your question contains its own answer. As long as the array is (expected to be) sufficiently dense, a dense representation will perform well. |
No. There can be multiple nullary constructors. data Result a =
Success a
| PermanentFailure
| Retry How common will that be? Dunno, but it's possible. |
Could GHC automatically apply this optimization to any unlifted data type (ping @sgraf812) that contains of at most one data constructor with exactly one lifted argument, and arbitrary many nullary constrictors. Then GHC could implement them as you describe: static pointers for the nullary ones, and simply the underlying argument pointer for the lifted constructor. And because this data type has a different kind than the types you can nest within, there is no risk of confusion with nested Nothings. The implementation could be confined to the STG to Cmm phase, I believe, with different code to match and create these values. (This suggestion was brought to you on the phone during breakfast, so I may miss something.) This seems to have similarities with #530, but seems independent. @clyring @AndreasPK |
Connection to denesting proposal #530Indeed this feels very close to #530, but I'm doubtful that we could tweak the denesting idea to data types like data Lazy (a :: UnliftedType) = Lazy a then data SMaybe a = SNothing | SJust {-# DENEST #-} !a then we can no longer denest Distinguished elementsThe "distinguished element" idea here is a different one: Since Indeed, it is impossible for GHC to allocate a pointer of type Unfortunately, GHC might also argue that due to that aliasing rule, a pointer comparison between different types will never return true and optimise Regarding problem (2): I don't see how we can meaningfully get around checking for multiple null values. I don't think it matters much until it does (let's work from concrete evidence/profiles). Regarding problem (3): Yes, the lazy version indeed looks broken, turning every thunk into an One simple solution to obscure the no alias realtionship would be to use OPAQUE (NOINLINE should already be sufficient, but this is about intent): ptrEqHomo :: a -> a -> Int#
ptrEqHomo a b = reallyUnsafePtrEquality# a b
{-# OPAQUE ptrEqHomo #-}
ptrEq :: a -> b -> Bool
ptrEq a b = isTrue# (ptrEqHomo a (unsafeCoerce b))
{-# INLINE ptrEq #-} Or advocate for (a new variant of?) A more involved way to put the library solution on firm grounds that is forward compatible with optimisation shenanigans would be to provide a builtin, auto derived type class (like class Distinguishable a
distinguishedRubbish :: a and that could be used for pointer comparisons with the guarantee that Indeed we already have something in Core that is already quite similar:
Automatic "distinguished element" optimisationThe idea of Joachim sounds like a generalisation of Rust's null pointer optimisation. I don't think it works well for polymorphism. E.g. data SMaybe a = SNothing | SJust !a
f :: a -> Int
f a = I# (getTag a)
Conclusion
|
@sgraf812 This is quite a substantial analysis, and I won't have time to read it until later today, but I wanted to respond to a few points:
Why not? Doesn't (or couldn't) GHC lay out the nullary representatives of a type in a contiguous memory block? As you say, maybe it won't be a big deal in practice because people won't use types with many constructors for this purpose, but
I'm sorry for the confusion. The lazy version was expressed a bit sloppily. It would be cleaner to describe it in terms of type UMaybe :: Type -> UnliftedType
data UMaybe a = UNothing | UJust a That is, the maybeness is not lazy; only the contents are lazy. |
FWIW, there is I think as far as we stay in It would be great to be able to have top-level unlifted value bindings (and syntactically values could be enough - GHC already allows that for With that, you can program in a strict Haskell using tricks from strict languages: like a distinguished pointer one. GHC also uses EDIT: if one really wants to speedup n-distinguished comparison, then asking for EDIT2: Is there a |
@sgraf812 To be very precise operationally, the idea is that my |
I guess you are right, it's theoretically to do such checks in O(1). But doing so would be quite an extensive feature for GHC, from exposing the proper primops to their implementation in the code generator. I'd rather say we sit it out until we have a client pushing for this feature.
Ah yes, that makes a lot more sense, working around the "can't work for a data type that is both lifted and has a lifted (non-strict) field" issue. Perhaps edit your OP with that Maybe type when you find time. Note that this only works as long as you can control that you never need to store an Anyway, since this is not the run-of-the-mill |
@sgraf812 data (forall x. a ≠ Shmaybe x) => Shmaybe a
= Shnothing | Shjust a but that's very much not the perspective I'm taking, and it's really not what I want—polymorphism goes out the window. The "outside in" approach doesn't suffer from the nesting problem, but I don't know how to think about automating it, if that's possible at all. |
In my suggestion I was also hoping that by making (In my idea above, case analysis would work by looking at pointer values, not at the tag, and thus
Is your If so, isn’t |
@nomeata Pointer tagging is an important optimization for unlifted sum types as well as all lifted types. |
Hmm, ok. I was assuming that for your use case you'd be willing to give up pointer tagging for this special data type to get the distinguished element optimization. Because if you don't, how can you do anything at all (which I guess is Sebastian's point). |
@nomeata I think I must have misunderstood you. Nevertheless, I don't think it should be necessary to give up pointer tagging. The pointer tag will mean different things depending on whether the value is a distinguished one or not, but that can be determined before actually following the pointer. |
I'm not sure that the pointer tag means anything useful (after all the UJust thing, being just the underlying pointer, can have any pointer tag value, can it?). But the pointer itself has enough information to discern the constructors, so one doesn't have to follow the pointer either - not exactly pointer tagging, but the same effect. |
@nomeata the pointer tag (if set) indicates either which distinguished value it is or which constructor of the underlying type. If there's only one distinguished value, or the relevant distinguished values are arranged contiguously, then it's quite cheap to determine which situation applies. |
I guess the "which distinguished value" aspect of the tag is useless, so never mind that. It's only useful if it's not a distinguished value. |
Right, but you first have to check if it's even in that range, as the arbitrary pointer could else have a similar tag, so you have to look at the full pointer (not just the tags), but don't have to follow it. I think we are jn agreement? What I don't know is whether we can even have different such “pointer bits interpretation strategies” for different (unlifted) data types. |
Yes, we agree. I don't see any issue with having different tag interpretation strategies in this context; the tagging itself is essentially the same, right? |
If it comes with a requirement for the type to be statically know we can. Otherwise probably not in a way that's desirable.
I argued we should have such a thing in the past but GHC currently doesn't support anything like that. If I understand this proposal correctly the goal here is to enable This would prevent getTag from working in a polymorphic context but allow this distinguished value optimization. Because the implementation for any such type could be given a special method that returns the semantically correct tag eve if operationally we have a different tag. That is we could have interface along the lines of: mkDistinguishedValue :: Proxy (WithDistinguishedValue a) -> WithDistinguishedValue a
wrapWithDistinguishedValue :: a -> WithDistinguishedValue a
isDistinguishedValue :: WithDistinguishedValue a -> Bool
instance GetTag (WithDistinguishedValue a) where
getTag x = if isDistinguishedValue x then 1 else 2 I see two possible ways to implement this sort of thing: Using one definite "distinguishedValue" that is statically allocated once and for all and used by all types. Which would break if one would nest Using one definite "distinguishedValue" for every concrete data type. This could be done implicitly by having WithDistinguishedValue be a derivable typeclass where the instance contains the distinguished value. This wouldn't be much different from what Typeable does and I think that's more or less what rusts implementation does. |
This has also been previously discussed at ghc issue 21726, and perhaps elsewhere. The library code makes a hidden assumption that there any two
This is haskell/core-libraries-committee#104, which has been accepted. So there's no problem with giving
For there to be no risk of confusion, the unlifted There is a version of this idea that should be safe: Representing an unboxed sum I'll also mention that in a less polymorphic context there are usually efficient designs available for working with null-like dummy values that don't need data RawObject n s where
Obj :: SmallMutableArray# s Any -> RawObject n s
Null :: RawObject MaybeNull s
newtype Object s = MkObject (forall n. RawObject n s)
newtype NullableObject s = MkNullable (RawObject MaybeNull s)
runObject :: Object s -> SmallMutableArray# s Any
runObject (MkObject v) = case v @DefinitelyNotNull of
Obj a -> a
toNullable :: Object s -> NullableObject s
toNullable (MkObject v) = MkNullable (v @MaybeNull) |
Yuck! Why not check for staticness when compacting? |
Static regions can only point to data within the region by design as far as I know. Iirc this is so that it can be stored/loaded to outside the heap. So even if we check it would entail some code randomly failing to compact. Which isn't all that promising. |
@AndreasPK I don't understand. Compact regions can only be inspected by identical binaries, which I imagine should have identical static data in identical places. |
It seems I was wrong about this: The compaction code can and does sometimes copy static data but it tries not to for things that cannot reference a CAF. My apologies for presenting this mis-information. I remember testing this at some point but perhaps I made a mistake while so doing. (Mind, it's not entirely obvious to me that it is even feasible to not clone static data: Remember that deserialization of compact objects needs to perform a pointer-fixup pass because in general the deserialized blocks can end up at different locations in memory than they were for the original serialized compact object.) Maybe this |
It's been said that null pointers were a billion dollar mistake. I see it a bit differently—misuse of null pointers was a billion dollar mistake. Nulls are often used in inferior languages as a distinguished value in an arbitrary type. This is trouble for all sorts of well known reasons. In Haskell, we have types like
Maybe
to do that better. However, null pointers are very useful for something else: adding a distinguished value to each element of a container. Doing this withMaybe
or similar is quite expensive, and doing it with null pointers is harmless. As @ekmett has explained to me, the way to do this in Haskell is withunsafeCoerce
. For example, here's a type like a strict array of strict maybes.This will be much more compact and efficient than an array of
SMaybe
s.Here's a version with lazy
Maybe
s:Note also that this technique can support multiple null pointers—just add more constructors to the
Null
type and check for each of them.So what's wrong? There are three problems:
Can we add some magic to support this better? Imagine we have some primops to do so, whatever those may look like. How do we deal with the multiple constructor issue? Require the type of nulls to match up with the type of
SMaybe
things. A single unsigned comparison should be able to determine whether a pointer is a null, and if so, a simple pointer offset should take us from the null constructor to its correspondingSMaybe
. The other way around should work as well. I don't know how to address the fragility of the lazy version.The text was updated successfully, but these errors were encountered: