Closed Bug 72722 Opened 24 years ago Closed 23 years ago

pldhash needs a C++ wrapper, which should be nsHashtable

Categories

(Core :: XPCOM, defect, P3)

defect

Tracking

()

RESOLVED WONTFIX
mozilla0.9.9

People

(Reporter: sfraser_bugs, Assigned: brendan)

References

Details

(Keywords: memory-footprint, perf)

Attachments

(4 files, 1 obsolete file)

pldhash is not widely used, desipte being a more efficient hash than plhash/ nsHashtable. I think the reasons are: 1. Lack of a C++ API forces clients to have to learn a non-transparent API 2. It can't own C++ objects, including reference-counted ones. I'm trying to write a wrapper that addresses these issues.
I'm not working on this bug, yet sfraser kindly is. So I think he should take assignment, at least for now. /be
Assignee: brendan → sfraser
Plus, I'm a C luddite and proud of it. /be
Also, plhash beats plhash in some cases, see http://lxr.mozilla.org/mozilla/source/js/src/jsdhash.h#94 and below. And do not mix up nsHashtable (sic) with plhash, on which it is layered. nsHashtable is much more malloc intensive for common key types than plhash is by default (if not using PLHashAllocOps). Muddling the C++ bloat of nsHashtable with plhash may put people off of using plhash, when it is the best choice. If C++ users can't handle using C from time to time, they need to Get Over It. /be
s/plhash beats plhash/plhash beats pldhash/ /be
I'm too much of a wimp to do this. It's gonna be easier to write a better API over plhash, and try to do fewer allocations than nsHashtable.
Status: NEW → RESOLVED
Closed: 24 years ago
Resolution: --- → WONTFIX
I'm actually convinced (luddite tho I may be) that it's possible to switch nsHashtable etc. over to pldhash. Some placement new allocation is required. Anyway, I'm gonna try. It seems very wrong to me to buy off on plhash's malloc happy ways, even if we fix the current extra malloc-happiness in nsHashtable. It also seems wrong to me to do a warren-style jihad thru the tree to change all ns*Hashtable uses. It's all just code, right? :-) /be
Status: RESOLVED → REOPENED
Resolution: WONTFIX → ---
Taking from sfraser. /be
Assignee: sfraser → brendan
Status: REOPENED → NEW
Keywords: mozilla1.0
Priority: -- → P3
Target Milestone: --- → mozilla0.9
nsHashKey is evil. In spite of its bogus vtbl pointer overhead for specialization that is always per-table, not per-key. There are no heterogenous tables in sight, and no one wants them either (proof: check each key subclass's Equals method impl). The problem with moving key specialization dispatching from the key class into the table class is that it combines with the various existing table subclasses, which specialize value type (void*, nsISupports* strong ref, "object" that's cloned and deleted, but not explicitly refcounted). My C++-fu is not up to expressing this kind of key X value specialization matrix in a series of table subtypes or templates. So I'm leaving the virtual methods in nsHashKey (actually, adding a SizeOf one, needed to set PLDHashTable.entrySize). But the win is still there. Patch and bloatblame numbers coming up. /be
Summary: pldhash needs a C++ wrapper, and should be able to own objects → pldhash needs a C++ wrapper, which should be nsHashtable
Somewhat related: I think pldhash needs an 'entry init' callback. The reason is that the return value from PL_DHashTableOperate(table, key, PL_DHASH_ADD); does not indicate whether space for this entry was just reserved, or whether an existing entry was returned (since PL_DHASH_ENTRY_IS_BUSY(entry) is true in both cases). If I'm trying to store objects inline in the table, I thus cannot tell if I have to call the constructor here or not. So a callback that indicate that space is being allocated for an entry for the first time would be useful. This would balance calls to PLDHashClearEntry. I guess it would be OK to overload PLDHashMoveEntry(), can call it with a NULL from* to indicate entry initialization.
Attached patch very lightly tested patch! (deleted) — Splinter Review
sfraser: the original design zeroed new entry store, and hoped that was enough for callers to distinguish between already-there and just-added after a successful return JS_DHASH_ADD return. Lean and mean. Sufficient? If zeroes could mean either just-added or already-there, what construction is required? /be
> what construction is required? Setting up vtables for classes with virtual methods in the entries, for one. I think you have to call the ctor to get these set up. (I found this out the hard way trying to use nsString in entries). Also, what happens when a newly added entry falls into a space that was previously occupied? Is it zero'd out again? Or left as the ClearEntry callback left it?
Left as the clearEntry callback left it -- hence the "clear" trope. Entry store starts zeroed and should go back to zeroed (except for keyHash, of course). Overloading moveEntry is ugly. I'll add a patch here for an initEntry hook. /be
Sounds good. Looking at your patch.
nsDeviceContextMac needs these additional changes: - - PRUnichar* str = (((FontNameKey*)aKey)->mString).ToNewUnicode(); + FontNameKey *fontNameKey = (FontNameKey *)aKey; + PRUnichar* str = nsCRT::strndup(fontNameKey->GetString(), fontNameKey-> GetStringLength()); if (!str) { for (j = j - 1; j >= 0; j--) { @@ -1181,8 +1147,10 @@ int j = info->mCount; + FontNameKey *fontNameKey = (FontNameKey *)aKey; + short fondID = (short) aData; ScriptCode script = ::FontToScript(fondID); if(script == info->mScript) { - PRUnichar* str = (((FontNameKey*)aKey)->mString).ToNewUnicode(); + PRUnichar* str = nsCRT::strndup(fontNameKey->GetString(), fontNameKey-> GetStringLength()); if (!str) { for (j = j - 1; j >= 0; j--) {
Another feature request. It's hard to go between nsSimpleEnumerator-style enumeration (HasMoreElements/ GetNext) and pl(d)hash-style (enumerator with callback.). nsHashTableEnumerator attempts this with a klutzy converter callback, and by copying all the entries into a flat array. However, pldhash internally has a nice linear data structure that is easy to enumerate over, and even passes entry index back to the enum callback. So an additional API call, like: PLDHashEntryHdr * PL_DHashTableGetIndexedEntry(PLDHashTable *table, PRUint32 index) would allow me to easily go between indices and entries. The final icing-on-the-cake would be a call to get the number of live entries: PRUint32 PL_DHashGetNumEntries(PLDHashTable *table). Or is this too all-signing all-dancing for you C luddites? (Yes, I know I can do both of the above with enumeractor calls, and that the first one, at least, implies something about the internal structure of the hash that it can fairly efficiently go between index and entry).
I bugged brendan about this, and came up with a solution that's in bug 68213. It's rough and ready, basically wraps a C++-style enumerator around the raw table. Here's a sketch: class MyCollection { PLDHashTable mTable; struct MyEntry { PLDHashEntryHdr mHdr; ... }; class iterator { protected: PLDHashTable* mTable; MyEntry* mCurrent; friend class MyCollection; // to access ctor iterator(PLDHashTable* aTable, MyEntry* aEntry) : mTable(aTable), mCurrect(aEntry) {} void Prev(); void Next(); public: iterator(const iterator&); iterator& operator=(const iterator&) const; // etc. MyEntry& operator*() { return *mCurrent; } MyEntry* operator->() { return mCurrent; } iterator& operator++() { Next(); return *this; } operator& operator--() { Prev(); return *this; } // post[in|de]crement similar PRBool operator==(const iterator& aOther) const { return aOther.mCurrent == mCurrent; } }; iterator First(); iterator Last(); } iterator MyCollection::First() { MyEntry* limit = NS_REINTERPRET_CAST(MyEntry*, mTable.entryStore); limit += PR_BIT(mTable.sizeLog2); MyEntry* entry = limit; while (++entry < limit) { if (PL_DHASH_ENTRY_IS_LIVE(&entry->mHdr)) break; } return iterator(&mTable, entry); } void MyCollection::Last() { MyEntry* limit = NS_REINTERPRET_CAST(MyEntry*, mTable.entryStore); limit += PR_BIT(mTable.sizeLog2); return iterator(&mTable, limit); } void MyCollection::iterator::Next() { MyEntry* limit = NS_REINTERPRET_CAST(MyEntry*, mTable->entryStore); limit += PR_BIT(mTable->sizeLog2); while (++mCurrent < limit) { if (PL_DHASH_ENTRY_IS_LIVE(&mCurrent->mHdr)) break; } } The PL_DHASH_ENTRY_IS_LIVE() macro is just the ENTRY_IS_LIVE macro from pldhash.c, pulled up to the top level.
Argh! That'll teach my to try to mangle a file. iterator MyCollection::First() { MyEntry* entry = NS_REINTERPRET_CAST(MyEntry*, mTable.entryStore); MyEntry* limit = entry; limit += PR_BIT(mTable.sizeLog2); do { if (PL_DHASH_ENTRY_IS_LIVE(&entry->mHdr)) break; } while (++entry < limit); return iterator(&mTable, entry); }
sfraser: entrySize and entryCount are public (yes, I trust you with sharp tools such as C structs). Patch coming up to make the is-entry-live? macro public, as well as to add a backward-compatible initEntry JSDHashTableOps member. /be
Sorry if my last comment was obscure: entryCount is the live entry count; you can use entryStore (and possibly entrySize) to stride over the table; and with the last patch, you can skip entries for which JS_DHASH_ENTRY_IS_LIVE evaluates to false. Let a thousand iterators bloom. The jsdhash.[ch] patch, in addition to adding the optional initEntry hook, finds the added entry that caused a table growth or compression. The current, top of trunk code skipped this entry in ChangeTable, worried about clearing it if it consumed the last free entry if ChangeTable failed, and re-added it in order to return the correct entry address in case of growth or compression. The patched version uses an in-out param to ChangeTable to find the new entry address given the old one. /be
Apropos entryCount: I was horribly abusing it to delay PL_DHashTableInit from the nsHashtable ctor to first Put -- it conveyed the desired size passed in the aInitSize arg to the ctor. This broke lots of stuff. New patch coming up, it also includes Simon's Mac fixes. /be
This stuff breaks Mac: - nsAutoString times; times.AssignWithConversion("Times"); - nsAutoString timesNewRoman; timesNewRoman.AssignWithConversion("Times New Roman"); - nsAutoString timesRoman; timesRoman.AssignWithConversion("Times Roman"); - nsAutoString arial; arial.AssignWithConversion("Arial"); - nsAutoString helvetica; helvetica.AssignWithConversion("Helvetica"); - nsAutoString courier; courier.AssignWithConversion("Courier"); - nsAutoString courierNew; courierNew.AssignWithConversion("Courier New"); - nsAutoString nullStr; + NS_NAMED_LITERAL_STRING(times, "Times"); + NS_NAMED_LITERAL_STRING(timesNewRoman, "Times New Roman"); + NS_NAMED_LITERAL_STRING(timesRoman, "Times Roman"); + NS_NAMED_LITERAL_STRING(arial, "Arial"); + NS_NAMED_LITERAL_STRING(helvetica, "Helvetica"); + NS_NAMED_LITERAL_STRING(courier, "Courier"); + NS_NAMED_LITERAL_STRING(courierNew, "Courier New"); + NS_NAMED_LITERAL_STRING(nullStr, "");
Last patch, minus the stuff I pasted above, seems to work well. However, it does warning a lot about: pldhash: for the table at address 0x0x080c6f04, the given entrySize of 28 probably favors chaining over double hashing.
That last patch leaks like a sieve, for want of the right static type for the key in each entry, or (equivalently) a clearEntry routine specialized per key. I'm working on the real fix, which squishes out the bogus vtbl pointer per key subtype altogether. Hope to have a patch later today. /be
With this patch, basic smoke tests pass for me. I had to make stored keys (in PLDHashTable entries) "flyweight", with Store, Clear, Wrap and Unwrap virtual methods on the heavier, usually stack-allocated nsHashKey subclasses for stored key setting, clearing, and wrapping by a dummy fat key. The dummy key is used per table to proxy for each flyweight as it needs to have virtual methods called on its key-data. The common patterns in certain methods (Clone, e.g.) suggest using templates. But I'd like to get this patch in, with more testing and measurement of the bloatblame wins. More tomorrow. Comments welcome. /be
r=valeski on the nsStreamConverter* changes if you are able to successfully view a html ftp dir listing (causes this code to be driven). Thanks for cleaning things up in there. Steps: 1. set "network.dir.generate_html" to true. 2. visit an ftp site ("ftp://ftp.mozilla.org") If you can sucessfully view that dir listing in html (not XUL), you've driven this code and it still works :-).
valeski: thanks, that all works great (confirming what I said on irc here, for posterity). Anyone else care to test the patch? Who else should r= which files? Scott, can you sr=? I'm working on the bloatblame numbers still. /be
Sorry for the delay on this. I'm looking at it now (or right after this dentist appointment, anyway); comments by this afternoon.
First, comments on |nsCRT::HashCode|. There's a fine line to walk here. In general, thre are a great many transformations one might want to perform on a string to be used as a key. Case folding is chief among them; but consider also stripping white space, control sequences, canonicalizing URLs etc. Case folding isn't much work, but you have made every caller pay for it on every call. This may be acceptable, though consider that other callers will have gone to some trouble to perform all key transformations in advance. The three choices you might pick among are: force all transformations in advance (the old code); optionally perform the most common transformation inline (your code); provide additional implementations of |HashCode| that do case-folding so only callers who want it pay. Does it matter? Morally, yes, at least to my eye. In performance? Probably not, only measuring would say. Additionally, you'd proabably move the |ToUpper| into the |W <= 0x007F| clause. if ( W <= 0x007F ) { W = nsCRT::ToUpper(W); code_length = 1; } else if ... else ... U = W; Still looking at the rest of the patch.
scc: the old code ripped off the broken nsCRT::HashCode algorithm and inlined a call to nsCRT::ToUpper. That seems wrong, even given the old alg's small code size compared to the UTF-8 one you wrote that fixed all the bad old woes. So the choices are: inline the new alg with ToUpper specialization, add a flag, add a new method that differs only from nsCRT::HashCode in the ToUpper call. I chose to add a flag, and would wager (yes, bet) that the cycle difference is in the noise. But I agree it's something to measure, perhaps just by watching jrgm's numbers. What do you say? I don't think it's a moral issue (right and wrong are unclear -- code space is precious too -- what is the acceptable cost of a human life, er, of a cycle saved in terms of instruction space bytes added?). I put the conditional ToUpper call in the single-PRUnichar path because nsCRT::ToUpper takes a PRUnichar, so must be valid on all values in that type's domain. Further, the old FontAliasKey::HashCode used that overloading of ToUpper, and as erik@netscape.com reminded me, it expected the case-folding to apply to non-ASCII code points too. Were you thinking that I was calling the char-param version of nsCRT::ToUpper? /be /be
Keywords: mozilla0.9
OS: Mac System 8.5 → All
scc wants to comment on the C++-allocator fu, but here's the first round of my comments. It appears that wrap and unwrap intentionally don't hold refs on their contents -- is that the case? What happens if you call Unwrap on a non-dummy key? If nothing else, a little bit of documentation for key-class authors would be nice, because this is likely to confuse. nsHashtable.cpp If you're asserting ownership over that file (by moving to 4-space indentation), could you also standardize on |nsHashkey *foo| rather than mixing in |nsHashkey* foo|? -PRUint32 nsCRT::HashCode(const char* str, PRUint32* resultingStrLen) +PRUint32 nsCRT::HashCode(const char* str, PRUint32* resultingStrLen, PRBool aIgnoreCase) The 80th column lies bleeding! - while ( (c = *s++) ) + while ( (c = *s++) ) { + if (aIgnoreCase) + c = nsCRT::ToUpper(char(c)); h = (h>>28) ^ (h<<4) ^ c; + } Is it worth duplicating the while loop for aIgnoreCase to avoid the branch in the loop, or (more likely) does it get lost in the function call cost? +(* PR_CALLBACK PLDHashClearEntry)(PLDHashTable *table, + PLDHashEntryHdr *entry); Bogus indentation in {pl,js}dhash.h. + // XXXbe shouldn't we do this in an Init method that can propagate + // NS_ERROR_OUT_OF_MEMORY? + PR_ASSERT(mLock != nsnull); Uh, "yes". Wanna file a separate bug that requires Init() to be called for all nsHashtables? + // shouldn't be adding an item during enumeration + PR_ASSERT(!mEnumerating); Is this documented anywhere? At least the attribute-affects-style code adds to hash table B while enumerating hash table A, and takes no special care to avoid A == B. (Though I don't think it happens In Real Life, because it's part of stylesheet cloning.) + // if this vertex has already been discovered, we don't want + // to leak it. (non-discovered vertex's get cleaned up when + // they're popped). Lotsa cleanup and better error handling in the stream converter, I notice; nice. I know you're just reformatting, but do you really want the blame for "vertex's"? + if (!entry) { + if (aStatus) + *aStatus = NS_ERROR_OUT_OF_MEMORY; ... + } else if (!aKey->Store((void *) &entry->mKeySpace)) { + // Store must have left mKeySpace in a valid state for Clear. + if (aStatus) + *aStatus = NS_ERROR_OUT_OF_MEMORY; What about: nsresult dummy, *status = aStatus ? aStatus : &dummy; if (!entry) *status = NS_ERROR_OUT_OF_MEMORY; + // XXXbe locking? Should we expose nsHashtable.Lock() so that people can freeze the table during cloning and enumeration operations? -PRIntn PR_CALLBACK -nsSupportsHashtable::EnumerateCopy(PLHashEntry *he, PRIntn i, void *arg) +static PLDHashOperator PR_CALLBACK +_supportsEnumerateCopy(PLDHashTable *table, PLDHashEntryHdr *hdr, + PRUint32 number, void *arg) Isn't _leadingUnderscore verboten? Why not just SupportsEnumerateCopy?
Blocks: 73308
More work needed, ran out of time. I'll have a new patch for next week. /be
Status: NEW → ASSIGNED
Keywords: mozilla0.9mozilla0.9.1
Target Milestone: mozilla0.9 → mozilla0.9.1
This patch is proving painful to keep fresh. I'll come up with a better patch for 0.9.1, or mothball it. /be
Patch is rusting, no time to strip the rust for 0.9.1. /be
Target Milestone: mozilla0.9.1 → mozilla0.9.2
Adding the perf keyword, and cc:ing some other people who might be interested as well. The Style System makes _heavy_ use of nsHashtable. Any help here could pay some dividents elsewhere...
Keywords: perf
Target Milestone: mozilla0.9.2 → mozilla0.9.3
I don't like the working patch, because it overcomplicates nsHashKey and its subtypes in order to work around lack of template support. Maybe a portable templates guru can advise me? Anyway, not likely to happen in one milestone. /be
Keywords: mozilla0.9.2
Target Milestone: mozilla0.9.3 → mozilla0.9.5
Keywords: helpwanted
I *might* get to this in 0.9.6. /be
Target Milestone: mozilla0.9.5 → mozilla0.9.6
Moving to 0.9.7, will fix then. /be
Keywords: footprint
Target Milestone: mozilla0.9.6 → mozilla0.9.7
I have a patch which uses nsFixedSizeAllocator to allocate the PLHashEntrys.. would that be useful here, either as a short-term solution, or as a step towards continuing PLHashTable use? One thought I had on making the nsHashKey subclasses less malloc-happy was to: - have some virtual sizeOf() method, which returned the space needed to clone the key - make Clone() always happen in-place That way the hashtable could allocate the space from a pool, and any other consumers of Clone() could allocate the space themselves with operator new
Attached patch use nsFixedSizeAllocator for PLHashEntries (obsolete) (deleted) — Splinter Review
Here's a small patch which we could check in now, and at least fix some small malloc crazyness. I also added MOZ_COUNT_CTOR/MOZ_COUNT_DTOR to nsFixedSizeAllocator (even if we don't take that patch, can I get an sr= on the MOZ_COUNT_CTOR stuff?)
oh, and ignore that HASH_USE_ARENAS - that was when I was deciding between raw PLArena's and nsFixedSizeAllocator. I've removed the #if's from my tree. Also, it turns out that to get MOZ_COUNT_CTOR(), I have to #include "nsTraceRefCnt.h" to get the xul template builder to build, which is kinda ugly..but what can ya do.
alecf: do you have any measurements using mozilla/tools/trace-malloc or similar? I'm happy to go with your patch if it makes things better without adding too much code. The mPool member adds 8 words to each nsHashtable, OTOH. /be
Comment on attachment 57771 [details] [diff] [review] use nsFixedSizeAllocator for PLHashEntries Now that I understand the virtues of PLDHashTable, I think there's no reason to do this half-assed. Obsoleting my patch :)
Attachment #57771 - Attachment is obsolete: true
so what are the chances of reviving this patch? I like the general concept, even if it does complicate things a bit.
I'll take a fresh look next week. /be
Target Milestone: mozilla0.9.7 → mozilla0.9.8
Urgh. Not sure this will happen before 1.0. Feel free to help if you can. /be
Target Milestone: mozilla0.9.8 → mozilla0.9.9
I have filed bug 125849 with a PLDHash wrapper; I think the intent there is slightly different from the intent here. I want a few lean, mean classes to wrap PLDHashTable without (much) overhead for the common cases like mapping from string -> nsISupports*, string -> string, string -> void*.
jkeiser shows the way in bug 125849. I'm going to WONTFIX this bug. If we believe that, once bug 125849 is closed, we need a jihad through the tree to convert nsHashtable users to nsDoubleHashtable (or whatever it'll be called), a new bug should be filed. Cc'ing cathleen and dp in case nsHashtable and its malloc-happy key types are showing up on footprint radar, and just so cathleen can track this issue. (Fine point: I'm WONTFIXing the entire summary, including the "which should be nsHashtable" -- obviously, we'll have a C++ wrapper for pldhash shortly in xpcom, thanks to jkeiser. I could INVALIDate, but WONTFIX seems better: it says we choose not to change nsHashtable's API at this late date.) /be
Status: ASSIGNED → RESOLVED
Closed: 24 years ago23 years ago
Resolution: --- → WONTFIX
see bug 177318 which I just filed, to take a slightly less ambitious approach on this... I think PLDHashTable can still help us.
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: