digitalmars.D - LRU cache for ~=
- Andrei Alexandrescu (27/27) Oct 19 2009 I just wrote this to Sean and Walter and subsequently discussed it with
- dsimcha (12/39) Oct 19 2009 1. I assume the LRU cache would be thread-local, so no lock would be ne...
- Andrei Alexandrescu (12/50) Oct 19 2009 It's rather subtle. I'd figured it out in the morning and forgot it by
- Andrei Alexandrescu (7/17) Oct 19 2009 Let me stress a point harder that I think I expressed poorly:
- dsimcha (6/23) Oct 19 2009 I think I get it now, although it's very conservative. One more questio...
- Andrei Alexandrescu (8/33) Oct 19 2009 That's why we need experimentation. My gut tells me that linear search
- Fawzi Mohamed (5/43) Oct 19 2009 yes you are probably right
- Leandro Lucarella (17/53) Oct 19 2009 I think this is moving in the wrong direction. It's a patch, not
- Andrei Alexandrescu (5/52) Oct 19 2009 The LRU doesn't need GC.sizeOf. It could always conservatively
- Chris Nicholson-Sauls (4/42) Oct 19 2009 Its an interesting idea, and if I have time tonight I'll take a crack at...
- Jason House (5/26) Oct 19 2009 Shouldn't it be MRU (Most Recently Used)?
- Andrei Alexandrescu (9/46) Oct 19 2009 I think so.
- dsimcha (8/11) Oct 19 2009 Yes, I see LRU/MRU/whatever as a kludge to allow convenience without bei...
- Fawzi Mohamed (2/18) Oct 19 2009 I fully agree
- Andrei Alexandrescu (15/27) Oct 19 2009 But how about the opposite view. What if the *previous* implementation
- dsimcha (23/50) Oct 19 2009 Started playing w/ the implementation a little and I see a problem. Wha...
- Andrei Alexandrescu (6/13) Oct 19 2009 I think GC.collect may simply evict the entire cache. The collection
- Sean Kelly (2/6) Oct 19 2009 Right. The GC already has an LRU cache of size 1 (ie. LRU entry) for GC...
- dsimcha (4/17) Oct 19 2009 But then you have to copy the whole array again, likely triggering anoth...
- Andrei Alexandrescu (3/21) Oct 19 2009 This happens whether or not a cache is in use.
- dsimcha (11/32) Oct 19 2009 But the array isn't guaranteed to get reallocated immediately after *eve...
- Andrei Alexandrescu (8/44) Oct 19 2009 I don't think I understand this.
- dsimcha (9/53) Oct 19 2009 about
- Steven Schveighoffer (14/86) Oct 20 2009 The cache can't be "cleared" every time, or else you might as well only ...
- Robert Jacques (8/97) Oct 20 2009 If it was free and simple to only clear the affected entries, sure. But ...
- Steven Schveighoffer (14/22) Oct 20 2009 Why? All you have to do is check whether a block is referenced in the L...
- Robert Jacques (7/32) Oct 20 2009 I agree, but my mind hadn't gotten there yet. (It was thinking of the
- Sean Kelly (2/6) Oct 19 2009 I'm not sure I agree that they'd be safe. Given only a pointer to the h...
- Andrei Alexandrescu (5/15) Oct 19 2009 That's the beauty of the cache: with the cache in place, you may
- Rainer Deyke (4/13) Oct 19 2009 Your definition of "safe" is clearly not aligned with mine.
- Denis Koroskin (3/14) Oct 19 2009 Safe as in SafeD (i.e. no memory corruption) :)
- Walter Bright (3/4) Oct 19 2009 Right. The problems with other definitions of safe is they are too
- Brad Roberts (5/10) Oct 19 2009 There's SafeD, which has a fairly formal definition.
- Christopher Wright (3/11) Oct 20 2009 But a fairly generic name, which confuses people repeatedly. I'm not the...
- Fawzi Mohamed (10/14) Oct 20 2009 no (or as little as possible) undefined behaviour comes to mind.
- Rainer Deyke (6/22) Oct 19 2009 If the caller wasn't expecting the array to be modified, then that's a
- Andrei Alexandrescu (3/21) Oct 19 2009 [citation needed]
- Rainer Deyke (6/11) Oct 19 2009 I guess we need to define memory corruption first. "Memory corruption
- Andrei Alexandrescu (10/20) Oct 19 2009 Where's that quote from?
- Rainer Deyke (14/24) Oct 19 2009 It's my own attempt to define memory corruption. It's equivalent to the
- Andrei Alexandrescu (3/17) Oct 19 2009 What's yours?
- Bill Baxter (9/27) Oct 19 2009 Probably something like "safe from easy-to-write hard-to-debug
- Andrei Alexandrescu (4/29) Oct 19 2009 How to reconcile this viewpoint with that of people who find ~= all too
- Leandro Lucarella (12/41) Oct 19 2009 Use a proper array type, not a view (slice).
- Rainer Deyke (8/12) Oct 19 2009 Safety is not an absolute, but a question of degree. The harder it is
- Andrei Alexandrescu (5/15) Oct 19 2009 Well you got a point. I think I will from here on talk about "soundness"...
- Don (5/21) Oct 20 2009 "SafeD is easy to learn and it keeps the programmers away from undefined...
- Leandro Lucarella (11/44) Oct 19 2009 And for me this is enough to remove appending from slicing and having
- Sean Kelly (2/4) Oct 19 2009 They effectively do anyway, since the GC block sizes are in powers of 2 ...
- grauzone (32/36) Oct 19 2009 Sounds like a bad hack, but at least it would solve the issue about
- Andrei Alexandrescu (12/57) Oct 19 2009 Why are there chances those functions will remove "t" from the cache?
- Steven Schveighoffer (16/43) Oct 20 2009 This is a very good idea. Incidentally, you only need the upper bound
- Andrei Alexandrescu (14/67) Oct 20 2009 Awesome, didn't think of that. So now more cases are caught:
- Robert Jacques (7/73) Oct 20 2009 So you want to synchronize the ~= function? I thought the LRU would be
- Steven Schveighoffer (12/20) Oct 20 2009 quoting myself earlier:
- Andrei Alexandrescu (3/8) Oct 20 2009 I think the cache should be thread-local.
- Steven Schveighoffer (8/22) Oct 20 2009 It was one of the coolest parts of my original proposal :)
I just wrote this to Sean and Walter and subsequently discussed it with Walter. Walter thinks this should work. Does anyone have the time and inclination to test this out? It would involve hacking into druntime's implementation of ~= (I'm not sure what the function name is). I'd really appreciate this; I'm overloaded as it is. ================== In wake of the recent demise of T[new], I was thinking of finding ways of making ~= efficient for T[]. Currently ~= is slow because accessing GC.sizeOf(void*) acquires a global lock and generally must figure out a lot of things about the pointer to make a decision. Also, ~= is dangerous because it allows slices to stomp over other slices. I was thinking of solving these issues by keeping an LRU (Least Recently Used) cache inside the implementation of ~=. The LRU would only have a few entries (4-8) and would store the parameters of the last ~= calls, and their cached capacities. So whenever code calls arr ~= b, the LRU is searched first. If the system finds "arr" (both bounds) in the LRU, that means the cached capacity is correct and can solve the matter without an actual trip to the GC at all! Otherwise, do the deed and cache the new slice and the new capacity. This also solves the lack of safety: if you request a growth on an array you just grew, it's impossible to have a valid slice beyond that array. This LRU would allow us to keep the slice API as it currently is, and also at excellent efficiency. What do you think? Andrei
Oct 19 2009
== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articleI just wrote this to Sean and Walter and subsequently discussed it with Walter. Walter thinks this should work. Does anyone have the time and inclination to test this out? It would involve hacking into druntime's implementation of ~= (I'm not sure what the function name is). I'd really appreciate this; I'm overloaded as it is. ================== In wake of the recent demise of T[new], I was thinking of finding ways of making ~= efficient for T[]. Currently ~= is slow because accessing GC.sizeOf(void*) acquires a global lock and generally must figure out a lot of things about the pointer to make a decision. Also, ~= is dangerous because it allows slices to stomp over other slices. I was thinking of solving these issues by keeping an LRU (Least Recently Used) cache inside the implementation of ~=. The LRU would only have a few entries (4-8) and would store the parameters of the last ~= calls, and their cached capacities. So whenever code calls arr ~= b, the LRU is searched first. If the system finds "arr" (both bounds) in the LRU, that means the cached capacity is correct and can solve the matter without an actual trip to the GC at all! Otherwise, do the deed and cache the new slice and the new capacity. This also solves the lack of safety: if you request a growth on an array you just grew, it's impossible to have a valid slice beyond that array. This LRU would allow us to keep the slice API as it currently is, and also at excellent efficiency. What do you think? Andrei1. I assume the LRU cache would be thread-local, so no lock would be necessary? 2. I don't understand how this solves the safety problem: // foo lives on the heap b/c we've idup'd it. string foo = "This is only a test.".idup; string bar = foo[0..4]; bar ~= " is _not "; writeln(foo); // prints "This is _not a test." Having access to the capacity in an LRU cache doesn't help if I understand it correctly. 3. I'm pretty familiar with these parts of druntime and would probably be willing to do the implementation after I understand a few details of it a little better.
Oct 19 2009
dsimcha wrote:== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articleYes.I just wrote this to Sean and Walter and subsequently discussed it with Walter. Walter thinks this should work. Does anyone have the time and inclination to test this out? It would involve hacking into druntime's implementation of ~= (I'm not sure what the function name is). I'd really appreciate this; I'm overloaded as it is. ================== In wake of the recent demise of T[new], I was thinking of finding ways of making ~= efficient for T[]. Currently ~= is slow because accessing GC.sizeOf(void*) acquires a global lock and generally must figure out a lot of things about the pointer to make a decision. Also, ~= is dangerous because it allows slices to stomp over other slices. I was thinking of solving these issues by keeping an LRU (Least Recently Used) cache inside the implementation of ~=. The LRU would only have a few entries (4-8) and would store the parameters of the last ~= calls, and their cached capacities. So whenever code calls arr ~= b, the LRU is searched first. If the system finds "arr" (both bounds) in the LRU, that means the cached capacity is correct and can solve the matter without an actual trip to the GC at all! Otherwise, do the deed and cache the new slice and the new capacity. This also solves the lack of safety: if you request a growth on an array you just grew, it's impossible to have a valid slice beyond that array. This LRU would allow us to keep the slice API as it currently is, and also at excellent efficiency. What do you think? Andrei1. I assume the LRU cache would be thread-local, so no lock would be necessary?2. I don't understand how this solves the safety problem:It's rather subtle. I'd figured it out in the morning and forgot it by the time I explained to Walter.// foo lives on the heap b/c we've idup'd it. string foo = "This is only a test.".idup; string bar = foo[0..4]; bar ~= " is _not "; writeln(foo); // prints "This is _not a test."Upon the call to ~=, bar is not in the LRU cache so ~= conservatively reallocates it. As a rule of thumb (which takes care of the subtler issues): if a growing slice is not found in the LRU cache, it will always be conservatively reallocated. This is exactly because you don't know whether that slice was reduced from a larger slice.Having access to the capacity in an LRU cache doesn't help if I understand it correctly. 3. I'm pretty familiar with these parts of druntime and would probably be willing to do the implementation after I understand a few details of it a little better.That would be awesome!!! Andrei
Oct 19 2009
dsimcha wrote:2. I don't understand how this solves the safety problem: // foo lives on the heap b/c we've idup'd it. string foo = "This is only a test.".idup; string bar = foo[0..4]; bar ~= " is _not "; writeln(foo); // prints "This is _not a test." Having access to the capacity in an LRU cache doesn't help if I understand it correctly.Let me stress a point harder that I think I expressed poorly: The LRU cache stores the capacity of a given slice given _BOTH_ the slice's left and right bounds. If you later come with a slice that has only one correct bound, the LRU doesn't care about it. That's the safety tidbit. Andrei
Oct 19 2009
== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articledsimcha wrote:I think I get it now, although it's very conservative. One more question: Is this going to take the place of ArrayBuilder or be inaddition? The LRU is a good hack to preserve syntactic elegance and ease of use, but it's somewhat of a kludge nonetheless and I'd ideally still like to see a real ArrayBuilder with full array-like semantics if T[new] is definitely out.2. I don't understand how this solves the safety problem: // foo lives on the heap b/c we've idup'd it. string foo = "This is only a test.".idup; string bar = foo[0..4]; bar ~= " is _not "; writeln(foo); // prints "This is _not a test." Having access to the capacity in an LRU cache doesn't help if I understand it correctly.Let me stress a point harder that I think I expressed poorly: The LRU cache stores the capacity of a given slice given _BOTH_ the slice's left and right bounds. If you later come with a slice that has only one correct bound, the LRU doesn't care about it. That's the safety tidbit. Andrei
Oct 19 2009
dsimcha wrote:== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articleThat's why we need experimentation. My gut tells me that linear search will scram through 8 elements real fast, and if there's something in the cache it will almost always be in a top position. Also, I think the LRU will solve all cases that matter.dsimcha wrote:I think I get it now, although it's very conservative.2. I don't understand how this solves the safety problem: // foo lives on the heap b/c we've idup'd it. string foo = "This is only a test.".idup; string bar = foo[0..4]; bar ~= " is _not "; writeln(foo); // prints "This is _not a test." Having access to the capacity in an LRU cache doesn't help if I understand it correctly.Let me stress a point harder that I think I expressed poorly: The LRU cache stores the capacity of a given slice given _BOTH_ the slice's left and right bounds. If you later come with a slice that has only one correct bound, the LRU doesn't care about it. That's the safety tidbit. AndreiOne more question: Is this going to take the place of ArrayBuilder or be inaddition? The LRU is a good hack to preserve syntactic elegance and ease of use, but it's somewhat of a kludge nonetheless and I'd ideally still like to see a real ArrayBuilder with full array-like semantics if T[new] is definitely out.Ideally we'd be able to render ArrayBuilder/Appender unnecessary. I think there is a need for a UniqueArray, but that's only loosely related. Andrei
Oct 19 2009
On 2009-10-19 21:53:53 +0200, Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> said:dsimcha wrote:yes you are probably right== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articleThat's why we need experimentation. My gut tells me that linear search will scram through 8 elements real fast, and if there's something in the cache it will almost always be in a top position. Also, I think the LRU will solve all cases that matter.dsimcha wrote:I think I get it now, although it's very conservative.2. I don't understand how this solves the safety problem: // foo lives on the heap b/c we've idup'd it. string foo = "This is only a test.".idup; string bar = foo[0..4]; bar ~= " is _not "; writeln(foo); // prints "This is _not a test." Having access to the capacity in an LRU cache doesn't help if I understand it correctly.Let me stress a point harder that I think I expressed poorly: The LRU cache stores the capacity of a given slice given _BOTH_ the slice's left and right bounds. If you later come with a slice that has only one correct bound, the LRU doesn't care about it. That's the safety tidbit. Andreia library array knowing its capacity is still useful I think. FawziOne more question: Is this going to take the place of ArrayBuilder or be inaddition? The LRU is a good hack to preserve syntactic elegance and ease of use, but it's somewhat of a kludge nonetheless and I'd ideally still like to see a real ArrayBuilder with full array-like semantics if T[new] is definitely out.Ideally we'd be able to render ArrayBuilder/Appender unnecessary. I think there is a need for a UniqueArray, but that's only loosely related.
Oct 19 2009
Andrei Alexandrescu, el 19 de octubre a las 13:51 me escribiste:I just wrote this to Sean and Walter and subsequently discussed it with Walter. Walter thinks this should work. Does anyone have the time and inclination to test this out? It would involve hacking into druntime's implementation of ~= (I'm not sure what the function name is). I'd really appreciate this; I'm overloaded as it is. ================== In wake of the recent demise of T[new], I was thinking of finding ways of making ~= efficient for T[]. Currently ~= is slow because accessing GC.sizeOf(void*) acquires a global lock and generally must figure out a lot of things about the pointer to make a decision. Also, ~= is dangerous because it allows slices to stomp over other slices. I was thinking of solving these issues by keeping an LRU (Least Recently Used) cache inside the implementation of ~=. The LRU would only have a few entries (4-8) and would store the parameters of the last ~= calls, and their cached capacities. So whenever code calls arr ~= b, the LRU is searched first. If the system finds "arr" (both bounds) in the LRU, that means the cached capacity is correct and can solve the matter without an actual trip to the GC at all! Otherwise, do the deed and cache the new slice and the new capacity. This also solves the lack of safety: if you request a growth on an array you just grew, it's impossible to have a valid slice beyond that array. This LRU would allow us to keep the slice API as it currently is, and also at excellent efficiency. What do you think?I think this is moving in the wrong direction. It's a patch, not a solution. And this makes dynamic arrays more coupled with the way the GC works. I think the GC shouldn't even provide a sizeOf(void*), we should move in that direction, removing restrictions to the GC instead of enforcing the current ones; at lest if the goal is to eventually have a better GC (a copying GC doesn't even have to store the size of the cell, for example). I hope you eventually realize that you are complicating things much more than necessary. -- Leandro Lucarella (AKA luca) http://llucax.com.ar/ ---------------------------------------------------------------------- GPG Key: 5F5A8D05 (F8CD F9A7 BF00 5431 4145 104C 949E BFB6 5F5A 8D05) ---------------------------------------------------------------------- ACCIDENTE FATAL EN FLORES, MUEREN DOS PERSONAS Y UN BOLIVIANO -- Crónica TV
Oct 19 2009
Leandro Lucarella wrote:Andrei Alexandrescu, el 19 de octubre a las 13:51 me escribiste:The LRU doesn't need GC.sizeOf. It could always conservatively reallocate whenever in doubt.I just wrote this to Sean and Walter and subsequently discussed it with Walter. Walter thinks this should work. Does anyone have the time and inclination to test this out? It would involve hacking into druntime's implementation of ~= (I'm not sure what the function name is). I'd really appreciate this; I'm overloaded as it is. ================== In wake of the recent demise of T[new], I was thinking of finding ways of making ~= efficient for T[]. Currently ~= is slow because accessing GC.sizeOf(void*) acquires a global lock and generally must figure out a lot of things about the pointer to make a decision. Also, ~= is dangerous because it allows slices to stomp over other slices. I was thinking of solving these issues by keeping an LRU (Least Recently Used) cache inside the implementation of ~=. The LRU would only have a few entries (4-8) and would store the parameters of the last ~= calls, and their cached capacities. So whenever code calls arr ~= b, the LRU is searched first. If the system finds "arr" (both bounds) in the LRU, that means the cached capacity is correct and can solve the matter without an actual trip to the GC at all! Otherwise, do the deed and cache the new slice and the new capacity. This also solves the lack of safety: if you request a growth on an array you just grew, it's impossible to have a valid slice beyond that array. This LRU would allow us to keep the slice API as it currently is, and also at excellent efficiency. What do you think?I think this is moving in the wrong direction. It's a patch, not a solution. And this makes dynamic arrays more coupled with the way the GC works. I think the GC shouldn't even provide a sizeOf(void*), we should move in that direction, removing restrictions to the GC instead of enforcing the current ones; at lest if the goal is to eventually have a better GC (a copying GC doesn't even have to store the size of the cell, for example).I hope you eventually realize that you are complicating things much more than necessary.I actually did a couple of days ago :o). Andrei
Oct 19 2009
Andrei Alexandrescu wrote:I just wrote this to Sean and Walter and subsequently discussed it with Walter. Walter thinks this should work. Does anyone have the time and inclination to test this out? It would involve hacking into druntime's implementation of ~= (I'm not sure what the function name is). I'd really appreciate this; I'm overloaded as it is. ================== In wake of the recent demise of T[new], I was thinking of finding ways of making ~= efficient for T[]. Currently ~= is slow because accessing GC.sizeOf(void*) acquires a global lock and generally must figure out a lot of things about the pointer to make a decision. Also, ~= is dangerous because it allows slices to stomp over other slices. I was thinking of solving these issues by keeping an LRU (Least Recently Used) cache inside the implementation of ~=. The LRU would only have a few entries (4-8) and would store the parameters of the last ~= calls, and their cached capacities. So whenever code calls arr ~= b, the LRU is searched first. If the system finds "arr" (both bounds) in the LRU, that means the cached capacity is correct and can solve the matter without an actual trip to the GC at all! Otherwise, do the deed and cache the new slice and the new capacity. This also solves the lack of safety: if you request a growth on an array you just grew, it's impossible to have a valid slice beyond that array. This LRU would allow us to keep the slice API as it currently is, and also at excellent efficiency. What do you think? AndreiIts an interesting idea, and if I have time tonight I'll take a crack at it. For those others who may, the function you care about is "_d_arrayappendcT" in compiler/dmd/lifetime. -- Chris Nicholson-Sauls
Oct 19 2009
Andrei Alexandrescu Wrote:I just wrote this to Sean and Walter and subsequently discussed it with Walter. Walter thinks this should work. Does anyone have the time and inclination to test this out? It would involve hacking into druntime's implementation of ~= (I'm not sure what the function name is). I'd really appreciate this; I'm overloaded as it is. ================== In wake of the recent demise of T[new], I was thinking of finding ways of making ~= efficient for T[]. Currently ~= is slow because accessing GC.sizeOf(void*) acquires a global lock and generally must figure out a lot of things about the pointer to make a decision. Also, ~= is dangerous because it allows slices to stomp over other slices. I was thinking of solving these issues by keeping an LRU (Least Recently Used) cache inside the implementation of ~=. The LRU would only have a few entries (4-8) and would store the parameters of the last ~= calls, and their cached capacities.Shouldn't it be MRU (Most Recently Used)? I assume ~= will be disallowed for shared arrays? Will all arrays overallicate when created in order to allow efficient appending? I feel like bolting on ~= to slices gives mediocre performance and a lot of gotchas. Some users may expect slices to be reference copied arrays and want to see appended data in other slices. Some may get away with mutating the array in the MRU or a slice and seeing the changes, only to get bitten later. A separate type really makes more sense to me...
Oct 19 2009
Jason House wrote:Andrei Alexandrescu Wrote:Yes. LRU would be the eviction strategy.I just wrote this to Sean and Walter and subsequently discussed it with Walter. Walter thinks this should work. Does anyone have the time and inclination to test this out? It would involve hacking into druntime's implementation of ~= (I'm not sure what the function name is). I'd really appreciate this; I'm overloaded as it is. ================== In wake of the recent demise of T[new], I was thinking of finding ways of making ~= efficient for T[]. Currently ~= is slow because accessing GC.sizeOf(void*) acquires a global lock and generally must figure out a lot of things about the pointer to make a decision. Also, ~= is dangerous because it allows slices to stomp over other slices. I was thinking of solving these issues by keeping an LRU (Least Recently Used) cache inside the implementation of ~=. The LRU would only have a few entries (4-8) and would store the parameters of the last ~= calls, and their cached capacities.Shouldn't it be MRU (Most Recently Used)?I assume ~= will be disallowed for shared arrays?I think so.Will all arrays overallicate when created in order to allow efficient appending?Upon the first append.I feel like bolting on ~= to slices gives mediocre performance and a lot of gotchas.You may be right about the gotchas, but you better produce some numbers wrt performance.Some users may expect slices to be reference copied arrays and want to see appended data in other slices. Some may get away with mutating the array in the MRU or a slice and seeing the changes, only to get bitten later. A separate type really makes more sense to me...Making ~= work well for slices does not preclude creating a distinct library type. Andrei
Oct 19 2009
== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articleMaking ~= work well for slices does not preclude creating a distinct library type. AndreiYes, I see LRU/MRU/whatever as a kludge to allow convenience without being unsafe egregiously inefficient. The fact that it is a kludge is not a criticism because there is clearly no easy answer, and thus a kludge is genuinely necessary. However, I think there needs to be a separate array builder type for "heavy duty" appending operations. In TDPL I would just say that concatenating slices can be inefficient and that people should use array builder for heavy duty appending, length changing, etc., w/o getting into the details.
Oct 19 2009
On 2009-10-20 00:13:05 +0200, dsimcha <dsimcha yahoo.com> said:== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articleI fully agreeMaking ~= work well for slices does not preclude creating a distinct library type. AndreiYes, I see LRU/MRU/whatever as a kludge to allow convenience without being unsafe egregiously inefficient. The fact that it is a kludge is not a criticism because there is clearly no easy answer, and thus a kludge is genuinely necessary. However, I think there needs to be a separate array builder type for "heavy duty" appending operations. In TDPL I would just say that concatenating slices can be inefficient and that people should use array builder for heavy duty appending, length changing, etc., w/o getting into the details.
Oct 19 2009
dsimcha wrote:== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articleBut how about the opposite view. What if the *previous* implementation of GC and ~= were a kludge that led to egregious inefficiency and revolting unsafety, kludge that that now is getting fixed. What I'm saying is that with the cache in place we'll have slices that are safe and efficient. Right now they are not safe and not efficient. I can hardly find reasons to characterize the new state of affairs as kludgey. One surprising (but safe) behavior that remains with slices is this: void fun(int[] a) { a[0] = 0; a ~= 42; a[0] = 42; } The caller may or may not see 42 in the first slot after the call. AndreiMaking ~= work well for slices does not preclude creating a distinct library type. AndreiYes, I see LRU/MRU/whatever as a kludge to allow convenience without being unsafe egregiously inefficient. The fact that it is a kludge is not a criticism because there is clearly no easy answer, and thus a kludge is genuinely necessary. However, I think there needs to be a separate array builder type for "heavy duty" appending operations. In TDPL I would just say that concatenating slices can be inefficient and that people should use array builder for heavy duty appending, length changing, etc., w/o getting into the details.
Oct 19 2009
== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articledsimcha wrote:Started playing w/ the implementation a little and I see a problem. What about the garbage collector? There are two possibilities: 1. The pointer gets stored as a pointer. The array never gets freed until it gets evicted from the cache. If it's a huge array, this is very bad. 2. The pointer gets cast to a size_t so it doesn't look like a reference. Then, we can have something like: string foo; foreach(i; 0..5) { foo ~= 'a'; } foo = null; GC.collect; // Now we have (foo.ptr, 5) in our LRU cache, but foo has been GC'd. string bar = "123456789"; // bar gets the memory reclaimed from foo. string baz = bar[0..5]; // Same length, ptr as foo. baz ~= '1'; // Finds stale information from foo in cache. writeln(bar); // Prints [1 2 3 4 5 1 7 8 9]. The only possible solutions I see would be to have the GC know everything about the LRU cache and evict stale entries (probably slows down GC a lot, a huge PITA to implement, couples things that shouldn't be tightly coupled), or clear the cache every time GC is run (probably would make appending so slow as to defeat the purpose of having the cache).== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articleBut how about the opposite view. What if the *previous* implementation of GC and ~= were a kludge that led to egregious inefficiency and revolting unsafety, kludge that that now is getting fixed. What I'm saying is that with the cache in place we'll have slices that are safe and efficient. Right now they are not safe and not efficient. I can hardly find reasons to characterize the new state of affairs as kludgey. One surprising (but safe) behavior that remains with slices is this: void fun(int[] a) { a[0] = 0; a ~= 42; a[0] = 42; } The caller may or may not see 42 in the first slot after the call. AndreiMaking ~= work well for slices does not preclude creating a distinct library type. AndreiYes, I see LRU/MRU/whatever as a kludge to allow convenience without being unsafe egregiously inefficient. The fact that it is a kludge is not a criticism because there is clearly no easy answer, and thus a kludge is genuinely necessary. However, I think there needs to be a separate array builder type for "heavy duty" appending operations. In TDPL I would just say that concatenating slices can be inefficient and that people should use array builder for heavy duty appending, length changing, etc., w/o getting into the details.
Oct 19 2009
dsimcha wrote:Started playing w/ the implementation a little and I see a problem. What about the garbage collector? There are two possibilities:[snip]The only possible solutions I see would be to have the GC know everything about the LRU cache and evict stale entries (probably slows down GC a lot, a huge PITA to implement, couples things that shouldn't be tightly coupled), or clear the cache every time GC is run (probably would make appending so slow as to defeat the purpose of having the cache).I think GC.collect may simply evict the entire cache. The collection cycle costs so much, the marginal cost of losing cached information is lost in the noise. Andrei
Oct 19 2009
Andrei Alexandrescu Wrote:I think GC.collect may simply evict the entire cache. The collection cycle costs so much, the marginal cost of losing cached information is lost in the noise.Right. The GC already has an LRU cache of size 1 (ie. LRU entry) for GC.sizeOf() and GC.query(), and these values are cleared on a collection by necessity, since pages can be repurposed if they're been completely emptied.
Oct 19 2009
== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articledsimcha wrote:But then you have to copy the whole array again, likely triggering another GC if the array is large. Then things really get ugly as, for all practical purposes, you've completely done away with the cache.Started playing w/ the implementation a little and I see a problem. What about the garbage collector? There are two possibilities:[snip]The only possible solutions I see would be to have the GC know everything about the LRU cache and evict stale entries (probably slows down GC a lot, a huge PITA to implement, couples things that shouldn't be tightly coupled), or clear the cache every time GC is run (probably would make appending so slow as to defeat the purpose of having the cache).I think GC.collect may simply evict the entire cache. The collection cycle costs so much, the marginal cost of losing cached information is lost in the noise. Andrei
Oct 19 2009
dsimcha wrote:== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articleThis happens whether or not a cache is in use. Andreidsimcha wrote:But then you have to copy the whole array again, likely triggering another GC if the array is large. Then things really get ugly as, for all practical purposes, you've completely done away with the cache.Started playing w/ the implementation a little and I see a problem. What about the garbage collector? There are two possibilities:[snip]The only possible solutions I see would be to have the GC know everything about the LRU cache and evict stale entries (probably slows down GC a lot, a huge PITA to implement, couples things that shouldn't be tightly coupled), or clear the cache every time GC is run (probably would make appending so slow as to defeat the purpose of having the cache).I think GC.collect may simply evict the entire cache. The collection cycle costs so much, the marginal cost of losing cached information is lost in the noise. Andrei
Oct 19 2009
== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articledsimcha wrote:defeat the== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articledsimcha wrote:Started playing w/ the implementation a little and I see a problem. What about the garbage collector? There are two possibilities:[snip]The only possible solutions I see would be to have the GC know everything about the LRU cache and evict stale entries (probably slows down GC a lot, a huge PITA to implement, couples things that shouldn't be tightly coupled), or clear the cache every time GC is run (probably would make appending so slow as toBut the array isn't guaranteed to get reallocated immediately after *every* GC run. If you're appending to a huge array, the GC will likely run several times while you're appending, leading to several unnecessary reallocations. Each of those unnecessary reallocations will increase the memory footprint of your program, possibly triggering another GC run and wiping out your cache again in short order, until, for sufficiently large arrays, a ~= b; is almost equivalent to a = a ~ b;This happens whether or not a cache is in use. AndreiBut then you have to copy the whole array again, likely triggering another GC if the array is large. Then things really get ugly as, for all practical purposes, you've completely done away with the cache.purpose of having the cache).I think GC.collect may simply evict the entire cache. The collection cycle costs so much, the marginal cost of losing cached information is lost in the noise. Andrei
Oct 19 2009
dsimcha wrote:== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articleI don't think I understand this. 1. Request for an append comes that runs out of memory 2. GC runs and clears memory 3. Array is reallocated and the capacity cached. No?dsimcha wrote:defeat the== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articledsimcha wrote:Started playing w/ the implementation a little and I see a problem. What about the garbage collector? There are two possibilities:[snip]The only possible solutions I see would be to have the GC know everything about the LRU cache and evict stale entries (probably slows down GC a lot, a huge PITA to implement, couples things that shouldn't be tightly coupled), or clear the cache every time GC is run (probably would make appending so slow as toBut the array isn't guaranteed to get reallocated immediately after *every* GC run. If you're appending to a huge array, the GC will likely run several times while you're appending, leading to several unnecessary reallocations.This happens whether or not a cache is in use. AndreiBut then you have to copy the whole array again, likely triggering another GC if the array is large. Then things really get ugly as, for all practical purposes, you've completely done away with the cache.purpose of having the cache).I think GC.collect may simply evict the entire cache. The collection cycle costs so much, the marginal cost of losing cached information is lost in the noise. AndreiEach of those unnecessary reallocations will increase the memory footprint of your program, possibly triggering another GC run and wiping out your cache again in short order, until, for sufficiently large arrays, a ~= b; is almost equivalent to a = a ~ b;I don't understand how the cache makes that all worse. Andrei
Oct 19 2009
== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articledsimcha wrote:about== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articledsimcha wrote:== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articledsimcha wrote:Started playing w/ the implementation a little and I see a problem. Whataboutthe garbage collector? There are two possibilities:[snip]The only possible solutions I see would be to have the GC know everythinghuge PITAthe LRU cache and evict stale entries (probably slows down GC a lot, aThis is entirely correct.I don't think I understand this. 1. Request for an append comes that runs out of memory 2. GC runs and clears memory 3. Array is reallocated and the capacity cached. No?defeat theto implement, couples things that shouldn't be tightly coupled), or clear the cache every time GC is run (probably would make appending so slow as toBut the array isn't guaranteed to get reallocated immediately after *every* GC run. If you're appending to a huge array, the GC will likely run several times while you're appending, leading to several unnecessary reallocations.This happens whether or not a cache is in use. AndreiBut then you have to copy the whole array again, likely triggering another GC if the array is large. Then things really get ugly as, for all practical purposes, you've completely done away with the cache.purpose of having the cache).I think GC.collect may simply evict the entire cache. The collection cycle costs so much, the marginal cost of losing cached information is lost in the noise. AndreiThe cache doesn't make anything *worse* than with no cache. The only point I'm trying to make is that, for large arrays, if the GC clears the cache every time it runs, things would start to get *almost as bad as* having no cache because the copy operations become expensive and the GC may run frequently.Each of those unnecessary reallocations will increase the memory footprint of your program, possibly triggering another GC run and wiping out your cache again in short order, until, for sufficiently large arrays, a ~= b; is almost equivalent to a = a ~ b;I don't understand how the cache makes that all worse. Andrei
Oct 19 2009
On Mon, 19 Oct 2009 22:37:26 -0400, dsimcha <dsimcha yahoo.com> wrote:== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articleThe cache can't be "cleared" every time, or else you might as well only keep one LRU entry: int[] twos, threes; for(int i = 1; i < 10000; i++) { twos ~= i * 2; threes ~= i * 3; } At some point, twos or threes needs an allocation triggering a collection, and that clears the cache, making the other array need an allocation, clearing the cache, etc. I'd think you only want to clear the entries affected by the collection. -Stevedsimcha wrote:about== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'sarticlearticledsimcha wrote:== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'sproblem. Whatdsimcha wrote:Started playing w/ the implementation a little and I see aabouteverythingthe garbage collector? There are two possibilities:[snip]The only possible solutions I see would be to have the GC knowhuge PITAlot, athe LRU cache and evict stale entries (probably slows down GC aThis is entirely correct.or clear theto implement, couples things that shouldn't be tightly coupled),as tocache every time GC is run (probably would make appending so slowdefeat thecollectionpurpose of having the cache).I think GC.collect may simply evict the entire cache. Theinformation iscycle costs so much, the marginal cost of losing cachedanother GC iflost in the noise. AndreiBut then you have to copy the whole array again, likely triggeringpractical purposes,the array is large. Then things really get ugly as, for all*every* GCBut the array isn't guaranteed to get reallocated immediately afteryou've completely done away with the cache.This happens whether or not a cache is in use. Andreirun. If you're appending to a huge array, the GC will likely runseveral timeswhile you're appending, leading to several unnecessary reallocations.I don't think I understand this. 1. Request for an append comes that runs out of memory 2. GC runs and clears memory 3. Array is reallocated and the capacity cached. No?The cache doesn't make anything *worse* than with no cache. The only point I'm trying to make is that, for large arrays, if the GC clears the cache every time it runs, things would start to get *almost as bad as* having no cache because the copy operations become expensive and the GC may run frequently.Each of those unnecessary reallocations will increase the memory footprint ofyourprogram, possibly triggering another GC run and wiping out your cacheagain inshort order, until, for sufficiently large arrays, a ~= b; is almost equivalent to a = a ~ b;I don't understand how the cache makes that all worse. Andrei
Oct 20 2009
On Tue, 20 Oct 2009 10:05:42 -0400, Steven Schveighoffer <schveiguy yahoo.com> wrote:On Mon, 19 Oct 2009 22:37:26 -0400, dsimcha <dsimcha yahoo.com> wrote:If it was free and simple to only clear the affected entries, sure. But doing so requires (very heavy?) modification of the GC in order to track and check changes. It also reduces collection performance. I think, that if GC allocations added entries to the LRU, and therefore the information in the LRU is never stale, you could avoid clearing the LRU. But this requires the LRU to be part of the GC.== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articleThe cache can't be "cleared" every time, or else you might as well only keep one LRU entry: int[] twos, threes; for(int i = 1; i < 10000; i++) { twos ~= i * 2; threes ~= i * 3; } At some point, twos or threes needs an allocation triggering a collection, and that clears the cache, making the other array need an allocation, clearing the cache, etc. I'd think you only want to clear the entries affected by the collection. -Stevedsimcha wrote:about== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'sarticle(SeeWebsiteForEmail erdani.org)'s articledsimcha wrote:== Quote from Andrei Alexandrescuproblem. Whatdsimcha wrote:Started playing w/ the implementation a little and I see aabouteverythingthe garbage collector? There are two possibilities:[snip]The only possible solutions I see would be to have the GC knowhuge PITAlot, athe LRU cache and evict stale entries (probably slows down GC aThis is entirely correct.or clear theto implement, couples things that shouldn't be tightly coupled),slow as tocache every time GC is run (probably would make appending sodefeat thecollectionpurpose of having the cache).I think GC.collect may simply evict the entire cache. Theinformation iscycle costs so much, the marginal cost of losing cachedanother GC iflost in the noise. AndreiBut then you have to copy the whole array again, likely triggeringpractical purposes,the array is large. Then things really get ugly as, for all*every* GCBut the array isn't guaranteed to get reallocated immediately afteryou've completely done away with the cache.This happens whether or not a cache is in use. Andreirun. If you're appending to a huge array, the GC will likely runseveral timeswhile you're appending, leading to several unnecessary reallocations.I don't think I understand this. 1. Request for an append comes that runs out of memory 2. GC runs and clears memory 3. Array is reallocated and the capacity cached. No?The cache doesn't make anything *worse* than with no cache. The only point I'm trying to make is that, for large arrays, if the GC clears the cache every time it runs, things would start to get *almost as bad as* having no cache because the copy operations become expensive and the GC may run frequently.Each of those unnecessary reallocations will increase the memory footprintof yourprogram, possibly triggering another GC run and wiping out yourcache again inshort order, until, for sufficiently large arrays, a ~= b; is almost equivalent to a = a ~ b;I don't understand how the cache makes that all worse. Andrei
Oct 20 2009
On Tue, 20 Oct 2009 10:48:31 -0400, Robert Jacques <sandford jhu.edu> wrote:On Tue, 20 Oct 2009 10:05:42 -0400, Steven Schveighoffer <schveiguy yahoo.com> wrote:Why? All you have to do is check whether a block is referenced in the LRU while freeing the block. I don't even think it would be that performance critical. Using my vastly novice assumptions about how the GC collection cycle works: step 1, mark all blocks that are not referenced by any roots. step 2, check which blocks are referenced by the LRU, if they are, then remove them from the LRU. step 3, recycle free blocks.I'd think you only want to clear the entries affected by the collection.If it was free and simple to only clear the affected entries, sure. But doing so requires (very heavy?) modification of the GC in order to track and check changes.But this requires the LRU to be part of the GC.I think we're already in that boat. If the LRU isn't attached to the GC, then ~= becomes a locking operation even if the GC is thread-local, which makes no sense. -Steve
Oct 20 2009
On Tue, 20 Oct 2009 11:24:21 -0400, Steven Schveighoffer <schveiguy yahoo.com> wrote:On Tue, 20 Oct 2009 10:48:31 -0400, Robert Jacques <sandford jhu.edu> wrote:I agree, but my mind hadn't gotten there yet. (It was thinking of the overhead of generational/concurrent collections, for some strange reason)On Tue, 20 Oct 2009 10:05:42 -0400, Steven Schveighoffer <schveiguy yahoo.com> wrote:Why? All you have to do is check whether a block is referenced in the LRU while freeing the block. I don't even think it would be that performance critical. Using my vastly novice assumptions about how the GC collection cycle works: step 1, mark all blocks that are not referenced by any roots. step 2, check which blocks are referenced by the LRU, if they are, then remove them from the LRU. step 3, recycle free blocks.I'd think you only want to clear the entries affected by the collection.If it was free and simple to only clear the affected entries, sure. But doing so requires (very heavy?) modification of the GC in order to track and check changes.Of course, Andrei just stated the cache should be thread-local (and probably in the function, not the GC) which throws a spanner into the works.But this requires the LRU to be part of the GC.I think we're already in that boat. If the LRU isn't attached to the GC, then ~= becomes a locking operation even if the GC is thread-local, which makes no sense. -Steve
Oct 20 2009
Andrei Alexandrescu Wrote:What I'm saying is that with the cache in place we'll have slices that are safe and efficient. Right now they are not safe and not efficient. I can hardly find reasons to characterize the new state of affairs as kludgey.I'm not sure I agree that they'd be safe. Given only a pointer to the head of a block there's no way to know whether it represents the array or a slice of the array from [0..n].
Oct 19 2009
Sean Kelly wrote:Andrei Alexandrescu Wrote:That's the beauty of the cache: with the cache in place, you may actually know that the memory beyond the end of a cached slice is seen by nobody else. AndreiWhat I'm saying is that with the cache in place we'll have slices that are safe and efficient. Right now they are not safe and not efficient. I can hardly find reasons to characterize the new state of affairs as kludgey.I'm not sure I agree that they'd be safe. Given only a pointer to the head of a block there's no way to know whether it represents the array or a slice of the array from [0..n].
Oct 19 2009
Andrei Alexandrescu wrote:One surprising (but safe) behavior that remains with slices is this: void fun(int[] a) { a[0] = 0; a ~= 42; a[0] = 42; } The caller may or may not see 42 in the first slot after the call.Your definition of "safe" is clearly not aligned with mine. -- Rainer Deyke - rainerd eldwood.com
Oct 19 2009
On Tue, 20 Oct 2009 03:00:57 +0400, Rainer Deyke <rainerd eldwood.com> wrote:Andrei Alexandrescu wrote:Safe as in SafeD (i.e. no memory corruption) :)One surprising (but safe) behavior that remains with slices is this: void fun(int[] a) { a[0] = 0; a ~= 42; a[0] = 42; } The caller may or may not see 42 in the first slot after the call.Your definition of "safe" is clearly not aligned with mine.
Oct 19 2009
Denis Koroskin wrote:Safe as in SafeD (i.e. no memory corruption) :)Right. The problems with other definitions of safe is they are too ill-defined.
Oct 19 2009
On Mon, 19 Oct 2009, Walter Bright wrote:Denis Koroskin wrote:There's SafeD, which has a fairly formal definition. The other side of it is the general principle of D which is that the right way should be the easy and obvious way. Slices and arrays have issue with the principle.Safe as in SafeD (i.e. no memory corruption) :)Right. The problems with other definitions of safe is they are too ill-defined.
Oct 19 2009
Brad Roberts wrote:On Mon, 19 Oct 2009, Walter Bright wrote:But a fairly generic name, which confuses people repeatedly. I'm not the first to recommend that the name be changed. It does more harm than good.Denis Koroskin wrote:There's SafeD, which has a fairly formal definition.Safe as in SafeD (i.e. no memory corruption) :)Right. The problems with other definitions of safe is they are too ill-defined.
Oct 20 2009
On 2009-10-20 03:41:59 +0200, Walter Bright <newshound1 digitalmars.com> said:Denis Koroskin wrote:no (or as little as possible) undefined behaviour comes to mind. Initialization of values in D follows that. Slice appending does not (and will remain so also with LRU cache). LRU cache improves the current status quo, but is still a cludge: undefined behavious in some corner cases, undefined optimization behaviour... It improves things, but cannot be trusted in general, thus a clean library type is still needed imho. FawziSafe as in SafeD (i.e. no memory corruption) :)Right. The problems with other definitions of safe is they are too ill-defined.
Oct 20 2009
Denis Koroskin wrote:On Tue, 20 Oct 2009 03:00:57 +0400, Rainer Deyke <rainerd eldwood.com> wrote:If the caller wasn't expecting the array to be modified, then that's a textbook case of memory corruption. If the caller /was/ expecting the array to be modified, then it's the opposite, which isn't much better. -- Rainer Deyke - rainerd eldwood.comAndrei Alexandrescu wrote:Safe as in SafeD (i.e. no memory corruption) :)One surprising (but safe) behavior that remains with slices is this: void fun(int[] a) { a[0] = 0; a ~= 42; a[0] = 42; } The caller may or may not see 42 in the first slot after the call.Your definition of "safe" is clearly not aligned with mine.
Oct 19 2009
Rainer Deyke wrote:Denis Koroskin wrote:[citation needed] AndreiOn Tue, 20 Oct 2009 03:00:57 +0400, Rainer Deyke <rainerd eldwood.com> wrote:If the caller wasn't expecting the array to be modified, then that's a textbook case of memory corruption.Andrei Alexandrescu wrote:Safe as in SafeD (i.e. no memory corruption) :)One surprising (but safe) behavior that remains with slices is this: void fun(int[] a) { a[0] = 0; a ~= 42; a[0] = 42; } The caller may or may not see 42 in the first slot after the call.Your definition of "safe" is clearly not aligned with mine.
Oct 19 2009
Andrei Alexandrescu wrote:Rainer Deyke wrote:I guess we need to define memory corruption first. "Memory corruption is when a piece of code erroneously overwrites memory." That applies here. Do you have a better definition? -- Rainer Deyke - rainerd eldwood.comIf the caller wasn't expecting the array to be modified, then that's a textbook case of memory corruption.[citation needed]
Oct 19 2009
Rainer Deyke wrote:Andrei Alexandrescu wrote:Where's that quote from? That definition is too vague to be of any use. Even an int that's written with a value when the spec meant to write another value is erroneously written and would be, by your definition, memory corruption. It is not.Rainer Deyke wrote:I guess we need to define memory corruption first. "Memory corruption is when a piece of code erroneously overwrites memory."If the caller wasn't expecting the array to be modified, then that's a textbook case of memory corruption.[citation needed]That applies here. Do you have a better definition?I do, but since you mentioned a textbook case of memory corruption, I was curious which textbook that wold come from. That's why I asked for a _citation_. A vague useless ad-hoc definition is easy to put together. Andrei
Oct 19 2009
Andrei Alexandrescu wrote:Rainer Deyke wrote:It's my own attempt to define memory corruption. It's equivalent to the definition in Wikipedia: "Memory corruption happens when the contents of a memory location are unintentionally modified due to programming errors."I guess we need to define memory corruption first. "Memory corruption is when a piece of code erroneously overwrites memory."Where's that quote from?Then post it.[...] Do you have a better definition?I do,but since you mentioned a textbook case of memory corruption, I was curious which textbook that wold come from.My use of the word "textbook" was idiomatic, not literal. From http://www.answers.com/topic/textbook: text·book (tĕkst'bʊk') [...] adj. Being a characteristic example of its kind; classic: a textbook case of schizophrenia. -- Rainer Deyke - rainerd eldwood.com
Oct 19 2009
Rainer Deyke wrote:Andrei Alexandrescu wrote:What's yours? AndreiOne surprising (but safe) behavior that remains with slices is this: void fun(int[] a) { a[0] = 0; a ~= 42; a[0] = 42; } The caller may or may not see 42 in the first slot after the call.Your definition of "safe" is clearly not aligned with mine.
Oct 19 2009
On Mon, Oct 19, 2009 at 5:07 PM, Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> wrote:Rainer Deyke wrote:Probably something like "safe from easy-to-write hard-to-debug programming errors". I agree that it would stink if all these changes were made and *still* one of the top gotchas with arrays/slices remained. It also makes me think slices and appending just don't belong together. Appending to a view ... just say no. --bbAndrei Alexandrescu wrote:What's yours?One surprising (but safe) behavior that remains with slices is this: void fun(int[] a) { =A0 a[0] =3D 0; =A0 a ~=3D 42; =A0 a[0] =3D 42; } The caller may or may not see 42 in the first slot after the call.Your definition of "safe" is clearly not aligned with mine.
Oct 19 2009
Bill Baxter wrote:On Mon, Oct 19, 2009 at 5:07 PM, Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> wrote:How to reconcile this viewpoint with that of people who find ~= all too darn convenient? AndreiRainer Deyke wrote:Probably something like "safe from easy-to-write hard-to-debug programming errors". I agree that it would stink if all these changes were made and *still* one of the top gotchas with arrays/slices remained. It also makes me think slices and appending just don't belong together. Appending to a view ... just say no.Andrei Alexandrescu wrote:What's yours?One surprising (but safe) behavior that remains with slices is this: void fun(int[] a) { a[0] = 0; a ~= 42; a[0] = 42; } The caller may or may not see 42 in the first slot after the call.Your definition of "safe" is clearly not aligned with mine.
Oct 19 2009
Andrei Alexandrescu, el 19 de octubre a las 20:12 me escribiste:Bill Baxter wrote:Use a proper array type, not a view (slice). -- Leandro Lucarella (AKA luca) http://llucax.com.ar/ ---------------------------------------------------------------------- GPG Key: 5F5A8D05 (F8CD F9A7 BF00 5431 4145 104C 949E BFB6 5F5A 8D05) ---------------------------------------------------------------------- No recuerdo las flores, no conozco el viento Aquí donde envejecen juntos presos y carceleros Los días no pasan, el tiempo no pasa Y vivimos contando el tiempo Y moriremos contando el tiempoOn Mon, Oct 19, 2009 at 5:07 PM, Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> wrote:How to reconcile this viewpoint with that of people who find ~= all too darn convenient?Rainer Deyke wrote:Probably something like "safe from easy-to-write hard-to-debug programming errors". I agree that it would stink if all these changes were made and *still* one of the top gotchas with arrays/slices remained. It also makes me think slices and appending just don't belong together. Appending to a view ... just say no.Andrei Alexandrescu wrote:What's yours?One surprising (but safe) behavior that remains with slices is this: void fun(int[] a) { a[0] = 0; a ~= 42; a[0] = 42; } The caller may or may not see 42 in the first slot after the call.Your definition of "safe" is clearly not aligned with mine.
Oct 19 2009
Andrei Alexandrescu wrote:Rainer Deyke wrote:Safety is not an absolute, but a question of degree. The harder it is to write incorrect code, the safer the language. One key element of this is deterministic behavior. If you rely on the whim of the runtime to determine if two slices refer to the same data, then it becomes much harder to reason about or test the code. -- Rainer Deyke - rainerd eldwood.comYour definition of "safe" is clearly not aligned with mine.What's yours?
Oct 19 2009
Rainer Deyke wrote:Andrei Alexandrescu wrote:Well you got a point. I think I will from here on talk about "soundness" instead of "safety", because the former is absolute.Rainer Deyke wrote:Safety is not an absolute, but a question of degree. The harder it is to write incorrect code, the safer the language.Your definition of "safe" is clearly not aligned with mine.What's yours?One key element of this is deterministic behavior. If you rely on the whim of the runtime to determine if two slices refer to the same data, then it becomes much harder to reason about or test the code.I agree. Andrei
Oct 19 2009
Andrei Alexandrescu wrote:Rainer Deyke wrote:"SafeD is easy to learn and it keeps the programmers away from undefined behaviors." -- safed.html. The behaviour you quoted is undefined behaviour, therefore it's not safe according to the only SafeD definition in the spec.Andrei Alexandrescu wrote:What's yours?One surprising (but safe) behavior that remains with slices is this: void fun(int[] a) { a[0] = 0; a ~= 42; a[0] = 42; } The caller may or may not see 42 in the first slot after the call.Your definition of "safe" is clearly not aligned with mine.
Oct 20 2009
Andrei Alexandrescu, el 19 de octubre a las 17:24 me escribiste:dsimcha wrote:And for me this is enough to remove appending from slicing and having a proper array type. There is no reason to keep that buggy behaviour. -- Leandro Lucarella (AKA luca) http://llucax.com.ar/ ---------------------------------------------------------------------- GPG Key: 5F5A8D05 (F8CD F9A7 BF00 5431 4145 104C 949E BFB6 5F5A 8D05) ---------------------------------------------------------------------- DONAN UN PENE EN NICARAGUA -- Crónica TV== Quote from Andrei Alexandrescu (SeeWebsiteForEmail erdani.org)'s articleBut how about the opposite view. What if the *previous* implementation of GC and ~= were a kludge that led to egregious inefficiency and revolting unsafety, kludge that that now is getting fixed. What I'm saying is that with the cache in place we'll have slices that are safe and efficient. Right now they are not safe and not efficient. I can hardly find reasons to characterize the new state of affairs as kludgey. One surprising (but safe) behavior that remains with slices is this: void fun(int[] a) { a[0] = 0; a ~= 42; a[0] = 42; } The caller may or may not see 42 in the first slot after the call.Making ~= work well for slices does not preclude creating a distinct library type. AndreiYes, I see LRU/MRU/whatever as a kludge to allow convenience without being unsafe egregiously inefficient. The fact that it is a kludge is not a criticism because there is clearly no easy answer, and thus a kludge is genuinely necessary. However, I think there needs to be a separate array builder type for "heavy duty" appending operations. In TDPL I would just say that concatenating slices can be inefficient and that people should use array builder for heavy duty appending, length changing, etc., w/o getting into the details.
Oct 19 2009
Jason House Wrote:Will all arrays overallicate when created in order to allow efficient appending?They effectively do anyway, since the GC block sizes are in powers of 2 and the runtime relies on the GC to determine block capacity. In effect, it's simply the GC that's over-allocating instead of the runtime itself.
Oct 19 2009
Andrei Alexandrescu wrote:I was thinking of solving these issues by keeping an LRU (Least Recently Used) cache inside the implementation of ~=. The LRU would only have a few entries (4-8) and would store the parameters of the last ~= calls, and their cached capacities.Sounds like a bad hack, but at least it would solve the issue about overwritten slices. But for some uses of array appending, you had to use a library based "Appender" (or whatever you have in Phobos 2): class Something { T[] data; void add(T x) { data ~= x; //chances that data are in the cache are minimal } } There's no "cache locality" or anything, but obviously you still would like to have efficient appender behavior. Also look at this: string[] t; t ~= somefunction(); t ~= someotherfunction(); Chances are high that those functions will remove "t" from the cache, and it would all be inefficient again. Nothing solved! Now you could try to make the cache function local to solve this issue. Whenever a function contains the ~= operator, the compiler allocates a "cache" struct, and the ~= operator passes a hidden pointer to it to the runtime. But that wouldn't work if you want pass slices to other functions and to append to them (but it would still be safe, I guess?). Looks like these performance hacks just don't work out. It'd be so much simpler to make "a~=b;" equivalent to "a=a~b;". Then there's no "mystery" about the performance of the ~= operator. You can explain everyone: "yes, this allocates; if you want performance, use ArrayAppender". Hey, you could alias this ArrayAppender type to something like T[new] to make it feel more natural. But then, we're at the beginning again.
Oct 19 2009
grauzone wrote:Andrei Alexandrescu wrote:Why is there no cache locality?I was thinking of solving these issues by keeping an LRU (Least Recently Used) cache inside the implementation of ~=. The LRU would only have a few entries (4-8) and would store the parameters of the last ~= calls, and their cached capacities.Sounds like a bad hack, but at least it would solve the issue about overwritten slices. But for some uses of array appending, you had to use a library based "Appender" (or whatever you have in Phobos 2): class Something { T[] data; void add(T x) { data ~= x; //chances that data are in the cache are minimal } } There's no "cache locality" or anything, but obviously you still would like to have efficient appender behavior.Also look at this: string[] t; t ~= somefunction(); t ~= someotherfunction(); Chances are high that those functions will remove "t" from the cache, and it would all be inefficient again. Nothing solved!Why are there chances those functions will remove "t" from the cache? For it to become a performance problem, there must be a loop with nine or more round-robin appends. When that does happen, yes, appends will become slower. (We may be able to make that better by using random eviction.)Now you could try to make the cache function local to solve this issue. Whenever a function contains the ~= operator, the compiler allocates a "cache" struct, and the ~= operator passes a hidden pointer to it to the runtime. But that wouldn't work if you want pass slices to other functions and to append to them (but it would still be safe, I guess?). Looks like these performance hacks just don't work out.Caches have a long and solid history of working. You'd have a very hard time convincing me that a cache that directly addresses a performance problem is a hack.It'd be so much simpler to make "a~=b;" equivalent to "a=a~b;". Then there's no "mystery" about the performance of the ~= operator. You can explain everyone: "yes, this allocates; if you want performance, use ArrayAppender". Hey, you could alias this ArrayAppender type to something like T[new] to make it feel more natural. But then, we're at the beginning again.I think making ~= faster is worth pursuing. Andrei
Oct 19 2009
On Mon, 19 Oct 2009 14:51:32 -0400, Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> wrote:I just wrote this to Sean and Walter and subsequently discussed it with Walter. Walter thinks this should work. Does anyone have the time and inclination to test this out? It would involve hacking into druntime's implementation of ~= (I'm not sure what the function name is). I'd really appreciate this; I'm overloaded as it is. ================== In wake of the recent demise of T[new], I was thinking of finding ways of making ~= efficient for T[]. Currently ~= is slow because accessing GC.sizeOf(void*) acquires a global lock and generally must figure out a lot of things about the pointer to make a decision. Also, ~= is dangerous because it allows slices to stomp over other slices. I was thinking of solving these issues by keeping an LRU (Least Recently Used) cache inside the implementation of ~=. The LRU would only have a few entries (4-8) and would store the parameters of the last ~= calls, and their cached capacities. So whenever code calls arr ~= b, the LRU is searched first. If the system finds "arr" (both bounds) in the LRU, that means the cached capacity is correct and can solve the matter without an actual trip to the GC at all! Otherwise, do the deed and cache the new slice and the new capacity. This also solves the lack of safety: if you request a growth on an array you just grew, it's impossible to have a valid slice beyond that array. This LRU would allow us to keep the slice API as it currently is, and also at excellent efficiency. What do you think?This is a very good idea. Incidentally, you only need the upper bound location, the beginning location is irrelevant, since you don't grow down. What do you do in the case where the memory was recycled? Does a GC collection cycle clean out the cache as well? This is better than my two previous ideas. The only drawback I see is if you have many threads doing appending, or you are appending more than 8 arrays at once in a round-robin fashion, you would lose all the benefit (although it shouldn't affect correctness). At that point however, you'd have to ask yourself why you aren't using a specialized appender type or function. In response to other's queries about how many LRUs to use, you'd probably want one per heap, and you'd want to lock/not lock based on whether the heap is thread local or not. -Steve
Oct 20 2009
Steven Schveighoffer wrote:On Mon, 19 Oct 2009 14:51:32 -0400, Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> wrote:Awesome, didn't think of that. So now more cases are caught: auto a = new int[100]; a ~= 42; a = a[50 .. $]; a ~= 52; That wouldn't have worked with my original suggestion, but it does work safely with yours.I just wrote this to Sean and Walter and subsequently discussed it with Walter. Walter thinks this should work. Does anyone have the time and inclination to test this out? It would involve hacking into druntime's implementation of ~= (I'm not sure what the function name is). I'd really appreciate this; I'm overloaded as it is. ================== In wake of the recent demise of T[new], I was thinking of finding ways of making ~= efficient for T[]. Currently ~= is slow because accessing GC.sizeOf(void*) acquires a global lock and generally must figure out a lot of things about the pointer to make a decision. Also, ~= is dangerous because it allows slices to stomp over other slices. I was thinking of solving these issues by keeping an LRU (Least Recently Used) cache inside the implementation of ~=. The LRU would only have a few entries (4-8) and would store the parameters of the last ~= calls, and their cached capacities. So whenever code calls arr ~= b, the LRU is searched first. If the system finds "arr" (both bounds) in the LRU, that means the cached capacity is correct and can solve the matter without an actual trip to the GC at all! Otherwise, do the deed and cache the new slice and the new capacity. This also solves the lack of safety: if you request a growth on an array you just grew, it's impossible to have a valid slice beyond that array. This LRU would allow us to keep the slice API as it currently is, and also at excellent efficiency. What do you think?This is a very good idea. Incidentally, you only need the upper bound location, the beginning location is irrelevant, since you don't grow down.What do you do in the case where the memory was recycled? Does a GC collection cycle clean out the cache as well?As you saw, there was some discussion about that as well.This is better than my two previous ideas. The only drawback I see is if you have many threads doing appending, or you are appending more than 8 arrays at once in a round-robin fashion, you would lose all the benefit (although it shouldn't affect correctness). At that point however, you'd have to ask yourself why you aren't using a specialized appender type or function.Yah. As I suspect a lot of code is actually doing round-robin naturally, I'm considering using a random eviction strategy. That way performance will degrade smoother. A more advanced algorithm would use introspection to choose dynamically between LRU and random. Andrei
Oct 20 2009
On Tue, 20 Oct 2009 10:14:52 -0400, Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> wrote:Steven Schveighoffer wrote:So you want to synchronize the ~= function? I thought the LRU would be thread local and therefore independent of these issues, as well as being faster. And if the LRU isn't thread-local, then why not make it part of the GC? It would both be more general and much simpler/cleaner to implement.On Mon, 19 Oct 2009 14:51:32 -0400, Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> wrote:Awesome, didn't think of that. So now more cases are caught: auto a = new int[100]; a ~= 42; a = a[50 .. $]; a ~= 52; That wouldn't have worked with my original suggestion, but it does work safely with yours.I just wrote this to Sean and Walter and subsequently discussed it with Walter. Walter thinks this should work. Does anyone have the time and inclination to test this out? It would involve hacking into druntime's implementation of ~= (I'm not sure what the function name is). I'd really appreciate this; I'm overloaded as it is. ================== In wake of the recent demise of T[new], I was thinking of finding ways of making ~= efficient for T[]. Currently ~= is slow because accessing GC.sizeOf(void*) acquires a global lock and generally must figure out a lot of things about the pointer to make a decision. Also, ~= is dangerous because it allows slices to stomp over other slices. I was thinking of solving these issues by keeping an LRU (Least Recently Used) cache inside the implementation of ~=. The LRU would only have a few entries (4-8) and would store the parameters of the last ~= calls, and their cached capacities. So whenever code calls arr ~= b, the LRU is searched first. If the system finds "arr" (both bounds) in the LRU, that means the cached capacity is correct and can solve the matter without an actual trip to the GC at all! Otherwise, do the deed and cache the new slice and the new capacity. This also solves the lack of safety: if you request a growth on an array you just grew, it's impossible to have a valid slice beyond that array. This LRU would allow us to keep the slice API as it currently is, and also at excellent efficiency. What do you think?This is a very good idea. Incidentally, you only need the upper bound location, the beginning location is irrelevant, since you don't grow down.What do you do in the case where the memory was recycled? Does a GC collection cycle clean out the cache as well?As you saw, there was some discussion about that as well.This is better than my two previous ideas. The only drawback I see is if you have many threads doing appending, or you are appending more than 8 arrays at once in a round-robin fashion, you would lose all the benefit (although it shouldn't affect correctness). At that point however, you'd have to ask yourself why you aren't using a specialized appender type or function.Yah. As I suspect a lot of code is actually doing round-robin naturally, I'm considering using a random eviction strategy. That way performance will degrade smoother. A more advanced algorithm would use introspection to choose dynamically between LRU and random. Andrei
Oct 20 2009
On Tue, 20 Oct 2009 10:37:40 -0400, Robert Jacques <sandford jhu.edu> wrote:So you want to synchronize the ~= function? I thought the LRU would be thread local and therefore independent of these issues, as well as being faster. And if the LRU isn't thread-local, then why not make it part of the GC? It would both be more general and much simpler/cleaner to implement.quoting myself earlier: On Tue, 20 Oct 2009 09:58:01 -0400, Steven Schveighoffer <schveiguy yahoo.com> wrote:In response to other's queries about how many LRUs to use, you'd probably want one per heap, and you'd want to lock/not lock based on whether the heap is thread local or not.You need a locked operation in the case where the heap is shared, otherwise, you lose safety. At the moment all we *have* is a shared heap. So ~= is a synchronized operation until thread-local heaps are available. I think the only logical place for the LRU is the GC, it makes no sense to have a a shared LRU for an unshared GC or vice versa. -Steve
Oct 20 2009
Robert Jacques wrote:So you want to synchronize the ~= function? I thought the LRU would be thread local and therefore independent of these issues, as well as being faster. And if the LRU isn't thread-local, then why not make it part of the GC? It would both be more general and much simpler/cleaner to implement.I think the cache should be thread-local. Andrei
Oct 20 2009
On Tue, 20 Oct 2009 10:14:52 -0400, Andrei Alexandrescu <SeeWebsiteForEmail erdani.org> wrote:Steven Schveighoffer wrote:It was one of the coolest parts of my original proposal :) http://www.digitalmars.com/webnews/newsgroups.php?art_group=digitalmars.D&article_id=63146 But using a cache solves a lot of the problems I didn't.This is a very good idea. Incidentally, you only need the upper bound location, the beginning location is irrelevant, since you don't grow down.Awesome, didn't think of that. So now more cases are caught: auto a = new int[100]; a ~= 42; a = a[50 .. $]; a ~= 52; That wouldn't have worked with my original suggestion, but it does work safely with yours.Yeah, I'm reading in thread order :) Still got 91 unread messages, so maybe I'll read all before replying again... -SteveWhat do you do in the case where the memory was recycled? Does a GC collection cycle clean out the cache as well?As you saw, there was some discussion about that as well.
Oct 20 2009