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

Improve deque, tables, and set modules #13620

Closed
wants to merge 20 commits into from
Closed

Conversation

PMunch
Copy link
Contributor

@PMunch PMunch commented Mar 10, 2020

The deque module has now gotten high, toDeque, and hash along with
improved documentation and improved examples.

All three modules now have their own default initial capacity, and these
are intdefines, so they can be changed by developers on compile-time for
performance tuning or running on low-end hardware.

@PMunch
Copy link
Contributor Author

PMunch commented Mar 10, 2020

Those build failures don't seem related to these changes

@timotheecour
Copy link
Member

timotheecour commented Mar 13, 2020

lots of good stuff in this PR.
Regarding the hash: thinking about this more, this is the wrong place to define such hash; there is nothing specific about deque in this hash definition; the only thing you're using is that there's a items defined for that type.

proposal 1 (simplest but not best)

hashes.nim should define:

proch hashIterable*[T](a: T): Hash =
  for ai in a:
    result = result !& hash(ai) # can later be improved via murmurhash
  result = !$h

and then:
proc deques.nim can define:

template hash*(a: Deque): untyped = hashIterable(a)

proposal 2 (best IMO)

yet another use case for {.enableif.} (#12048)
hash gets automatically defined for anything that has items defined, and we can define its implementation generically (using the most appropriate one depending on sizeof(ElementType(T)), eg jenkins one-at-a-time hash or murmurhash), eg:

proch hash*[T](a: T): Hash {.enableif: isIterable(a).} =
  for ai in a:
    result = result !& hash(ai) # can later be improved via murmurhash
  result = !$h

this may be possible using concepts but concepts have caveats, lots of bugs, exponential complexity issues and limitations, see #12048;

enableif is actually straightforward and incredibly simpler than concepts

@PMunch
Copy link
Contributor Author

PMunch commented Mar 13, 2020

Improved the hash procedure and fixed the naming convention for the intdefines. Whether or not the hashes should be improved is a bit out of the scope for this PR though so I didn't touch it.

@PMunch
Copy link
Contributor Author

PMunch commented Mar 13, 2020

Woops, didn't mean to change that..

@Araq
Copy link
Member

Araq commented Mar 14, 2020

No Azure testing was done on this! Investigating...

@timotheecour
Copy link
Member

timotheecour commented Mar 14, 2020

Revert accidental change to hashset hashing

this would actually not be correct, since elements are in arbitrary order; I'm fixing hash(HashSet) in #13649

@timotheecour
Copy link
Member

timotheecour commented Mar 14, 2020

No Azure testing was done on this! Investigating...

=> tracking here: #13653

Copy link
Member

@timotheecour timotheecour left a comment

Choose a reason for hiding this comment

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

LGTM

@PMunch
Copy link
Contributor Author

PMunch commented Mar 16, 2020

Again I don't think those errors are related to this PR

@PMunch
Copy link
Contributor Author

PMunch commented Mar 16, 2020

Rebased to current devel to see if the CI will pass now

@timotheecour
Copy link
Member

@PMunch failure seems unrelated:

2020-03-16T12:49:11.1016288Z C:\Users\VssAdministrator.nimble\pkgs\faststreams-0.1.0\faststreams\output_stream.nim(69, 70) Error: expression has no address; maybe use 'unsafeAddr'

but also I don't have this failure in other PRs, so maybe rebase once more?

@timotheecour
Copy link
Member

ping @PMunch

@timotheecour timotheecour added the stale Staled PR/issues; remove the label after fixing them label Mar 19, 2021
@timotheecour
Copy link
Member

timotheecour commented Mar 19, 2021

ping @PMunch (and needs rebase)

@stale stale bot removed the stale Staled PR/issues; remove the label after fixing them label Mar 19, 2021
@PMunch
Copy link
Contributor Author

PMunch commented Mar 19, 2021

Is there anything apart from rebase that needs doing?

@timotheecour
Copy link
Member

well i already gave LGTM, but it also depends on #13620 (comment), let's see what CI gives after rebase

The deque module has now gotten `high`, `toDeque`, and `hash` along with
improved documentation and improved examples.

All three modules now have their own default initial capacity, and these
are intdefines, so they can be changed by developers on compile-time for
performance tuning or running on low-end hardware.
@Araq
Copy link
Member

Araq commented Apr 7, 2022

Please rebase one more time, the CIs are finally green once again for devel.

@PMunch
Copy link
Contributor Author

PMunch commented Apr 7, 2022

Done, everything should be OK now

@Araq
Copy link
Member

Araq commented Apr 8, 2022

I've finally understood the implications of these changes and I don't like them anymore. Sorry. For performance tuning, use initTable etc with tuned initial sizes per callsite, not via even more defines.

@Araq Araq closed this Apr 8, 2022
@ringabout
Copy link
Member

Probably we can remove some compiles call in lib?

ref
make toSeq not use compiles feature:
nim-works/nimskull#151

remove some when compiles calls from tables/deques:
nim-works/nimskull#161

@PMunch
Copy link
Contributor Author

PMunch commented Apr 8, 2022

I've finally understood the implications of these changes and I don't like them anymore. Sorry. For performance tuning, use initTable etc with tuned initial sizes per callsite, not via even more defines.

Care to elaborate? I've submitted and maintained this PR for quite a while now so it's a bit disheartening to see it dropped with only a vague sentence as to why. Which implications? Surely you don't have anything against that I implemented hash for deques, or high, or even the improved performance of converting a seq to a deque? To be honest I'm not even sure why you're against splitting the default capacities up into different ones. The reason I did this was because I was trying to run something on a microcontroller but by default they created internal structures that where too big, even though I knew they would never grow that big. I figured it would be nice to tweak these parameters on demand, especially seeing how they don't really hinder anything..

@Araq Araq reopened this Apr 11, 2022
@Araq
Copy link
Member

Araq commented Apr 11, 2022

Hmm, let me try again: The initTable variants take an initial capacity parameter and as your PR showed changing that produces observable changes in behavior. People rely on the existing behavior, if only for the same reason that we do: Test cases exploit the hashing order and the order changed.

If the stdlib uses too much memory for your system because of these default capacities the stdlib should make use of the capacity parameters that already exist. Alternatively, you could change the default global value but please not with yet another -d switch.


template checkIfInitialized(deq: typed) =
when compiles(defaultInitialSize):
when declared(nimDequeDefaultInitialCapacity):
Copy link
Member

Choose a reason for hiding this comment

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

Pre-exists. But why this line is needed? It seems unnecessary. See also nim-works/nimskull#161 (comment)

@SolitudeSF
Copy link
Contributor

SolitudeSF commented Apr 11, 2022

but please not with yet another -d switch.

why? it doesnt introduce new dialect.

@PMunch
Copy link
Contributor Author

PMunch commented Apr 11, 2022

Thank you for reopening this. The issue I was running into wasn't with the stdlib, but rather with a 3rd party library. It is quite common in Nim to see people creating a new container without passing any size, or just not calling newX but rather have it done automatically when the first element is added. This means that everywhere, in every library, in all of the Nim ecosystem, this initial capacity is being used. And the initial value is fine for normal desktop usage, that little extra waste probably isn't causing anyone any harm. But if you suddenly have to be very careful about memory consumption then you have to go through every single library you might want to use and give it a way to pass an initial capacity to their underlying structures. Since this value is already stored in a const in the codebase I figured it wouldn't really hurt to make it an {.intdefine.}. I realise now that I messed up and took the default for sets and used it for both tables and sets, but that's an easy fix. If these defines are kept at the same sizes as they used to be then no-one would notice any difference as long as they don't touch that define. And what other way (that wouldn't require a special version of the standard library) could I change such a value?

@arnetheduck
Copy link
Contributor

arnetheduck commented Apr 11, 2022

-d switches are quite problematic: they don't compose well (in the sense that two libraries can't use competing values for them), meaning that they scale to small scripts / apps that don't do much, but not much further beyond that point.

Small applications, if it turns out they need a different setting, can trivially be changed (since they're small), meaning that the feature is not that useful in these cases.

Anything that uses a library (ie that solves a more complex problem and smartly reuses existing code instead of reinventing) cannot productively use -d flags at all: it's a global setting and there's no way to set one value for library A and another for library B - by necessity, they need the libraries to etiher expose library-specific flags or dynamic switches, meaning that the right place to introduce a fix is in that library.

Another way to put it is that -d flags are nice toys for micro-benchmarks, but not much else, and come at a cost. The long-term benefit is limited - the long-term maintenance overhead however is not - once it's there, it's hard to remove.

@SolitudeSF
Copy link
Contributor

and how does this particular set of -d switches makes things worse?

@PMunch
Copy link
Contributor Author

PMunch commented Apr 11, 2022

@arnetheduck, while I don't disagree with you in general this is one specific instance where it actually makes sense to use these flags. Any code, small or large which actually cares about what the initial size of these collections are can (and currently do) simply set the initial size in the constructor. These new flags have absolutely no interest to library creators. What these flags do offer, and where I start disagreeing with your sentiment, is the ability for an end-user to tweak the program they're writing for all those libraries that don't care what the initial size of their collections are. The only reason to change these values are for severely memory constrained targets (and perhaps extremely memory rich targets). All they do is change the initial capacity of containers that didn't bother to specify that they wanted a particular initial capacity. There are no dialects, no change of logic, nothing to really maintain. There's only a small flag being passed on to the memory allocator.

@Araq
Copy link
Member

Araq commented Apr 12, 2022

and how does this particular set of -d switches makes things worse?

We can eventually remove some of -d switches and make the code-base better. Or we can keep adding -d switches.

@PMunch
Copy link
Contributor Author

PMunch commented Apr 13, 2022

and how does this particular set of -d switches makes things worse?

We can eventually remove some of -d switches and make the code-base better. Or we can keep adding -d switches.

This doesn't really answer the question at all though. Why is fewer -d switches better? This switch doesn't require any code in the compiler to maintain, it is simply an intdefine in the standard library. The standard library is there to be used across a wide range of applications, and switches like this makes it more adaptable.

@arnetheduck
Copy link
Contributor

Why is fewer -d switches better?

because they (tend to) represent a very narrow use case (often it's "workaround for poorly designed library X"), while increasing test requirements exponentially - further, they have to be maintained indefinitely (once added, code starts relying on them) and this prevents more useful modifications being done to to standard library in general - std lib sitting at the root of every dependency chain out there multiplies this cost (as opposed to changes in "leaf" code)

@SolitudeSF
Copy link
Contributor

Still no real argument against these particular switches.

@Araq
Copy link
Member

Araq commented Apr 13, 2022

Still no real argument against these particular switches.

An argument against switches in general is an argurment against any particular switch. And especially it's an argument against adding yet more switches. Sounds like basic logic to me.

@Araq
Copy link
Member

Araq commented Apr 13, 2022

The standard library is there to be used across a wide range of applications, and switches like this makes it more adaptable.

Every switch needs to be documented and communicated. What's wrong with the idea to tweak the defaults for version 2.0 so that it works better for embedded? In my naive world, if it works on embedded it works well elsewhere too.

@PMunch
Copy link
Contributor Author

PMunch commented Apr 13, 2022

The standard library is there to be used across a wide range of applications, and switches like this makes it more adaptable.

Every switch needs to be documented and communicated. What's wrong with the idea to tweak the defaults for version 2.0 so that it works better for embedded? In my naive world, if it works on embedded it works well elsewhere too.

The problem here is that for embedded I'd probably want to set them to 1. Never allocate more than what's absolutely necessary. But these would be silly defaults for non-embedded as it would quickly need to reallocate for the first couple adds until it got up to a reasonable size (after all it's not set to 0 for a reason). Different platforms have different tradeoffs, and for that reason should be treated differently.

I get where the arguments against these switches come from, but in this particular case they don't make much sense. There's nothing more to test, hashing order isn't supposed to be deterministic anyways, so code that depends on it is already broken. If you actually care about the default size of a container simply check the corresponding constant in your code, or just set the initial size to something useful. These are implementation details, so no code should depend on them. And worst case scenario if they do depend on them it simply wouldn't work for certain values.

@stale
Copy link

stale bot commented May 20, 2023

This pull request has been automatically marked as stale because it has not had recent activity. If you think it is still a valid PR, please rebase it on the latest devel; otherwise it will be closed. Thank you for your contributions.

@stale stale bot added the stale Staled PR/issues; remove the label after fixing them label May 20, 2023
@github-actions
Copy link
Contributor

This pull request has been marked as stale and closed due to inactivity after 395 days.

@github-actions github-actions bot closed this Jun 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stale Staled PR/issues; remove the label after fixing them
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants