diff --git a/ChangeLog b/ChangeLog index 801671243..f92692741 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,12 @@ +2026-08-12 Todd White + + * Source/NSUserDefaults.m: Answer a default from a small table + kept by the calling thread when nothing has changed the defaults + since that thread last asked, so that reads do not serialise on + the instance lock. Every change to what a lookup can see + increments a generation counter, and an entry from an earlier + generation is not used. + 2026-08-12 Todd White * Source/NSBundle.m: Let a thread reuse the localization table it diff --git a/Source/NSUserDefaults.m b/Source/NSUserDefaults.m index b7d0b11d2..17057cf49 100644 --- a/Source/NSUserDefaults.m +++ b/Source/NSUserDefaults.m @@ -72,6 +72,7 @@ @interface NSUserDefaultsWin32 : NSUserDefaults #endif #import "GSPrivate.h" +#import "GSPThread.h" /* Wait for access */ #define _MAX_COUNT 5 /* Max 10 sec. */ @@ -116,6 +117,74 @@ @interface NSUserDefaultsWin32 : NSUserDefaults */ static int flags[GSUserDefaultMaxFlag] = { 0 }; +/* Every change to what a lookup can see increments this counter while the + * lock of the defaults object making the change is held, and a thread keeps + * the answers it has already been given along with the value the counter had + * when it got them. An answer from an earlier generation is never used, so a + * thread whose defaults have not changed since it last looked answers from + * its own table and does not take the lock. A new defaults object increments + * the counter as it sets up its volatile domains, so an entry cannot survive + * into an unrelated object which happens to be allocated at the same address. + */ +#define GS_DEFAULTS_MEMO_SIZE 16 + +typedef struct { + NSUserDefaults *owner; // compared, never messaged + NSString *key; // retained + id value; // retained, nil is an answer + uint64_t generation; // zero while the entry is unused +} GSDefaultsMemoEntry; + +static uint64_t defaultsGeneration = 1; +static gs_thread_key_t defaultsMemoKey; +static BOOL defaultsMemoKeyReady = NO; + +static void +defaultsMemoFree(void *memo) +{ + GSDefaultsMemoEntry *entries = (GSDefaultsMemoEntry*)memo; + unsigned i; + + for (i = 0; i < GS_DEFAULTS_MEMO_SIZE; i++) + { + RELEASE(entries[i].key); + RELEASE(entries[i].value); + } + free(entries); +} + +static inline void +defaultsDidChange(void) +{ + __atomic_fetch_add(&defaultsGeneration, 1, __ATOMIC_RELEASE); +} + +static GSDefaultsMemoEntry* +defaultsMemo(void) +{ + GSDefaultsMemoEntry *memo; + + if (NO == __atomic_load_n(&defaultsMemoKeyReady, __ATOMIC_ACQUIRE)) + { + return 0; + } + memo = GS_THREAD_KEY_GET(defaultsMemoKey); + if (0 == memo) + { + memo = calloc(GS_DEFAULTS_MEMO_SIZE, sizeof(GSDefaultsMemoEntry)); + if (0 != memo) + { + GS_THREAD_KEY_SET(defaultsMemoKey, memo); + if (GS_THREAD_KEY_GET(defaultsMemoKey) != memo) + { + free(memo); + memo = 0; + } + } + } + return memo; +} + /* An instance of the GSPersistentDomain class is used to encapsulate * a single persistent domain (represented as a property list file in * the defaults directory. @@ -659,6 +728,10 @@ + (void) initialize beenHere = YES; initializing = YES; + if (GS_THREAD_KEY_INIT(defaultsMemoKey, defaultsMemoFree)) + { + __atomic_store_n(&defaultsMemoKeyReady, YES, __ATOMIC_RELEASE); + } nextObjectSel = @selector(nextObject); objectForKeySel = @selector(objectForKey:); addSel = @selector(addEntriesFromDictionary:); @@ -807,6 +880,7 @@ + (void) resetStandardUserDefaults { [defs->_lock lock]; [defs->_tempDomains setObject: regDefs forKey: NSRegistrationDomain]; + defaultsDidChange(); [defs->_lock unlock]; } } @@ -1028,6 +1102,7 @@ + (NSUserDefaults*) standardUserDefaults [defs persistentDomainForName: NSGlobalDomain]; [defs->_searchList addObject: GSConfigDomain]; [defs->_searchList addObject: NSRegistrationDomain]; + defaultsDidChange(); /* Load persistent data into the new instance. */ @@ -1049,6 +1124,7 @@ + (NSUserDefaults*) standardUserDefaults unsigned index = [defs->_searchList count] - 1; [defs->_searchList insertObject: lang atIndex: index]; + defaultsDidChange(); } /* Set up language constants */ @@ -1294,6 +1370,7 @@ - (id) initWithContentsOfFile: (NSString*)path setObject: [NSMutableDictionaryClass dictionaryWithCapacity: 10] forKey: NSRegistrationDomain]; [_tempDomains setObject: GNUstepConfig(nil) forKey: GSConfigDomain]; + defaultsDidChange(); updateCache(self); @@ -1379,6 +1456,7 @@ - (void) addSuiteNamed: (NSString*)aName NS_DURING { DESTROY(_dictionaryRep); + defaultsDidChange(); [_searchList removeObject: aName]; index = [_searchList indexOfObject: bundleIdentifier]; index = (index == NSNotFound) ? 0 : (index + 1); @@ -1477,7 +1555,23 @@ - (NSInteger) integerForKey: (NSString*)defaultName - (id) objectForKey: (NSString*)defaultName { - id object = nil; + id object = nil; + GSDefaultsMemoEntry *memo = defaultsMemo(); + GSDefaultsMemoEntry *entry = 0; + uint64_t generation = 0; + + if (0 != memo && nil != defaultName) + { + entry = memo + ([defaultName hash] % GS_DEFAULTS_MEMO_SIZE); + if (entry->owner == self + && entry->generation + == __atomic_load_n(&defaultsGeneration, __ATOMIC_ACQUIRE) + && (entry->key == defaultName + || [entry->key isEqual: defaultName])) + { + return AUTORELEASE(RETAIN(entry->value)); + } + } [_lock lock]; NS_DURING @@ -1505,6 +1599,7 @@ - (id) objectForKey: (NSString*)defaultName break; } RETAIN(object); + generation = __atomic_load_n(&defaultsGeneration, __ATOMIC_RELAXED); GS_ENDITEMBUF(); [_lock unlock]; } @@ -1514,6 +1609,19 @@ - (id) objectForKey: (NSString*)defaultName [localException raise]; } NS_ENDHANDLER + + if (0 != entry) + { + NSString *oldKey = entry->key; + id oldValue = entry->value; + + entry->owner = self; + entry->key = [defaultName copy]; + entry->value = RETAIN(object); + entry->generation = generation; + RELEASE(oldKey); + RELEASE(oldValue); + } return AUTORELEASE(object); } @@ -1790,6 +1898,7 @@ - (void) setSearchList: (NSArray*)newList NSString *n; DESTROY(_dictionaryRep); + defaultsDidChange(); RELEASE(_searchList); _searchList = [newList mutableCopy]; /* Ensure that any domains we need are loaded. @@ -2005,6 +2114,7 @@ - (BOOL) synchronize if (YES == haveChange) { DESTROY(_dictionaryRep); + defaultsDidChange(); } if (_changedDomains != nil) @@ -2112,6 +2222,7 @@ - (void) removeVolatileDomainForName: (NSString*)domainName NS_DURING { DESTROY(_dictionaryRep); + defaultsDidChange(); [_tempDomains removeObjectForKey: domainName]; if ([_searchList containsObject: domainName]) { @@ -2149,6 +2260,7 @@ - (void) setVolatileDomain: (NSDictionary*)domain } DESTROY(_dictionaryRep); + defaultsDidChange(); domain = [domain mutableCopy]; [_tempDomains setObject: domain forKey: domainName]; RELEASE(domain); @@ -2280,6 +2392,7 @@ - (void) registerDefaults: (NSDictionary*)newVals [_tempDomains setObject: regDefs forKey: NSRegistrationDomain]; } DESTROY(_dictionaryRep); + defaultsDidChange(); [regDefs addEntriesFromDictionary: newVals]; updateCache(self); [_lock unlock]; @@ -2303,6 +2416,7 @@ - (void) removeSuiteNamed: (NSString*)aName NS_DURING { DESTROY(_dictionaryRep); + defaultsDidChange(); [_searchList removeObject: aName]; updateCache(self); [_lock unlock]; @@ -2537,6 +2651,7 @@ - (void) _changePersistentDomain: (NSString*)domainName NS_DURING { DESTROY(_dictionaryRep); + defaultsDidChange(); if (_changedDomains == nil) { _changedDomains = [[NSMutableArray alloc] initWithObjects: &domainName @@ -2863,6 +2978,7 @@ - (BOOL) setObject: (id)anObject forKey: (NSString*)aKey [removed addObject: aKey]; } [contents removeObjectForKey: aKey]; + defaultsDidChange(); return YES; } else @@ -2890,6 +3006,7 @@ - (BOOL) setObject: (id)anObject forKey: (NSString*)aKey } } [contents setObject: anObject forKey: aKey]; + defaultsDidChange(); return YES; } } @@ -3007,6 +3124,7 @@ - (BOOL) synchronize } } ASSIGN(contents, disk); + defaultsDidChange(); } if (YES == hasLocalChanges) {