Skip to content

Stack overflow on calling GetHashCode on large list #1838

Closed

Description

@csmager

I stumbled on this by chance of having a large list as part of a record and something in WPF calling GetHashCode.

Repro steps

Run this code, with or without tail calls enabled

[0..5000000].GetHashCode()

Expected behavior

I would expect a result, perhaps even if only based on the first n items in the list.

Actual behavior

Execution terminates with StackOverflowException

Related information

VS 2015 Update 3
FSharp.Core 4.4.0.0
.NET Framework 4.6.1

Activity

  1. forki commented on Nov 25, 2016

    @forki
    Contributor

    I would expect a result, perhaps even if only based on the first n items in the list.

    Yes we should compute a result - but it needs all items.

  2. forki commented on Nov 25, 2016

    @forki
    Contributor

    @dsyme any hints to where I can find the impl?

  3. forki commented on Nov 25, 2016

    @forki
    Contributor

    does it fall back to IEnumerable.GetHashCode()?

  4. PatrickMcDonald commented on Nov 25, 2016

    @PatrickMcDonald
    Contributor

    @forki maybe we need all the items for backwards compatibility, but without a backwards compatibility requirement there might be a case for limiting the items included in the calculation

    e.g. (Seq.initInfinite id).GetHashCode() returns in a finite amount of time :)

  5. dsyme commented on Nov 26, 2016

    @dsyme
    Contributor

    @forki I think this will be hard to fix. Check the implementation of the GetHashCode override for the list type, generated by src\fsharp\AugmentWithHashCompare.fs. It should ideally be a tail-calling loop (turned into a real loop by IlxGen.fs)

  6. forki commented on Nov 26, 2016

    @forki
    Contributor

    tbh I can't fin the list case. I assume it uses mkUnionHashWithComparer!?

  7. dsyme commented on Nov 26, 2016

    @dsyme
    Contributor

    tbh I can't fin the list case. I assume it uses mkUnionHashWithComparer!?

    Yes

  8. dsyme commented on Nov 26, 2016

    @dsyme
    Contributor

    Can we confirm this is not a regression please? I'm pretty sure it isn't, indeed it is really a feature request?

  9. forki commented on Nov 27, 2016

    @forki
    Contributor

    It is a feature request that something doesn't blow up in stack overflow? ;-)

  10. forki commented on Nov 27, 2016

    @forki
    Contributor

    To be more serious: I think if we just return 0 instead of blowing up (somehow try catch the things) it would probably already solve many issues.

  11. dsyme commented on Nov 27, 2016

    @dsyme
    Contributor

    @forki The bahviour has to be backwards-compat, without changing the hash codes.

    IIRC it's hard to make this tail-recursive but I can't remember the details.

  12. abelbraaksma commented on Dec 4, 2016

    @abelbraaksma
    Contributor

    Can we confirm this is not a regression please? I'm pretty sure it isn't, indeed it is really a feature request?

    For the record, I can confirm this blows up in both debug and release builds using F# 4.0, VS2015, Win7, x86 and x64 builds (if that'd make a difference).

    I don't know why this needs to be hard to be tail-recursive. In fact, I think we owe it to the users to be tail-recursive, even if it is hard. Didn't Jon Harrop once claim that every recursive function can be turned into a recursive one? I'm no expert, but if that's true..., we should, right?

    The current algorithm (as I see it in Reflector on FSharpList<T>) is first calculating hash of tail (the recursion), then combines that with head and some bit shifting. Unless the requirement is that it must behave exactly the same (except for the uncatchable SO exception, sorry @forki, you cannot catch this one and then return 0), it shouldn't be too hard to reverse the order, or add a continuation.

  13. dsyme commented on Dec 5, 2016

    @dsyme
    Contributor

    I don't know why this needs to be hard to be tail-recursive.

    One easy solution is to hand-write the GetHashCoe implementations for F# lists, rather than rely on the generated augmentation code.

    The code should be written in such a way that the hash codes don't change from today

  14. abelbraaksma commented on Dec 5, 2016

    @abelbraaksma
    Contributor

    The code should be written in such a way that the hash codes don't change from today

    @dsyme, Not sure I follow. You mean with "from today" that historical hash codes should be equal, that is (hash [1;2] in vX must be integer-equal to hash [1;2] in vNext) or that BCL hash code must be integer-equal to whatever we come up with for F# lists?

  15. 26 remaining items

  16. abelbraaksma commented on Oct 4, 2020

    @abelbraaksma
    Contributor

    @krauthaufen, looking at the linked #9070, which was started to fix this, a fix lead to unexpected changes in code generation. I've had the intention to give it another look or use a different approach. I'll see what I can do.

    Workarounds involve things like wrapping List in a new DU where you override the GetHashCode method, or doing so in every type that includes list types. It's unf. easy to forget and easy to get wrong.

  17. cartermp commented on Oct 4, 2020

    @cartermp
    Contributor

    Yeah, unfortunately this one will be tricky without risking a source breaking or binary breaking change on one of the most-used data types in FSharp.Core: #9070 (comment)

    The alternative fix is to "flatten" the structural hash function generated for lists. Since a list is defined as recursive DU, this change would also affect all other recursive DUs unless it were specialized only to list. Either way it's tricky business.

  18. krauthaufen commented on Oct 4, 2020

    @krauthaufen
    Contributor

    Hey, personally I think that changing HashCode values for types is not really a breaking change but if you're commited to keep the hashcodes identical I think there will not really be a way to solve this with tail-recursion/loops.
    Generally speaking shouldn't all DU hashes be implemented accumulator-style s.t. cases like lists 'accidentially' are tail-recursive?

  19. cartermp commented on Oct 4, 2020

    @cartermp
    Contributor

    Note that my concerns have nothing to do with the actual hash code values. It's that the emitted API shape is different than if you explicitly implement those interfaces in List.

  20. Happypig375 commented on Jun 2, 2021

    @Happypig375
    Member

    Even though the hash function can also be used for arrays and get structural hashes, this still works but not for lists. Strange.

  21. added
    Area-LibraryIssues for FSharp.Core not covered elsewhere
    and removed on Mar 31, 2022
  22. abelbraaksma commented on Nov 30, 2022

    @abelbraaksma
    Contributor

    this still works but not for lists. Strange.

    lists are recursive data types. The generic hash implementation uses a generic, non tail recursive approach. Hence the SO exception. Solution is to either make the code TCO, or to special case lists (or both).

  23. abelbraaksma commented on Jan 7, 2023

    @abelbraaksma
    Contributor

    Awesome this got solved!! Gonna open the champagne now 😄. This one was a big deal :).

  24. voronoipotato commented on Feb 14, 2023

    @voronoipotato
    Contributor

    I missed that this was fixed! Amazing thank you so much for all your hard work.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area-LibraryIssues for FSharp.Core not covered elsewhereBugImpact-Medium(Internal MS Team use only) Describes an issue with moderate impact on existing code.

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions