Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
2026-08-12 Todd White <todd.white@thalion.global>

* 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 <todd.white@thalion.global>

* Source/NSBundle.m: Let a thread reuse the localization table it
Expand Down
120 changes: 119 additions & 1 deletion Source/NSUserDefaults.m
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ @interface NSUserDefaultsWin32 : NSUserDefaults
#endif

#import "GSPrivate.h"
#import "GSPThread.h"

/* Wait for access */
#define _MAX_COUNT 5 /* Max 10 sec. */
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:);
Expand Down Expand Up @@ -807,6 +880,7 @@ + (void) resetStandardUserDefaults
{
[defs->_lock lock];
[defs->_tempDomains setObject: regDefs forKey: NSRegistrationDomain];
defaultsDidChange();
[defs->_lock unlock];
}
}
Expand Down Expand Up @@ -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.
*/
Expand All @@ -1049,6 +1124,7 @@ + (NSUserDefaults*) standardUserDefaults
unsigned index = [defs->_searchList count] - 1;

[defs->_searchList insertObject: lang atIndex: index];
defaultsDidChange();
}

/* Set up language constants */
Expand Down Expand Up @@ -1294,6 +1370,7 @@ - (id) initWithContentsOfFile: (NSString*)path
setObject: [NSMutableDictionaryClass dictionaryWithCapacity: 10]
forKey: NSRegistrationDomain];
[_tempDomains setObject: GNUstepConfig(nil) forKey: GSConfigDomain];
defaultsDidChange();

updateCache(self);

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1505,6 +1599,7 @@ - (id) objectForKey: (NSString*)defaultName
break;
}
RETAIN(object);
generation = __atomic_load_n(&defaultsGeneration, __ATOMIC_RELAXED);
GS_ENDITEMBUF();
[_lock unlock];
}
Expand All @@ -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);
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -2005,6 +2114,7 @@ - (BOOL) synchronize
if (YES == haveChange)
{
DESTROY(_dictionaryRep);
defaultsDidChange();
}

if (_changedDomains != nil)
Expand Down Expand Up @@ -2112,6 +2222,7 @@ - (void) removeVolatileDomainForName: (NSString*)domainName
NS_DURING
{
DESTROY(_dictionaryRep);
defaultsDidChange();
[_tempDomains removeObjectForKey: domainName];
if ([_searchList containsObject: domainName])
{
Expand Down Expand Up @@ -2149,6 +2260,7 @@ - (void) setVolatileDomain: (NSDictionary*)domain
}

DESTROY(_dictionaryRep);
defaultsDidChange();
domain = [domain mutableCopy];
[_tempDomains setObject: domain forKey: domainName];
RELEASE(domain);
Expand Down Expand Up @@ -2280,6 +2392,7 @@ - (void) registerDefaults: (NSDictionary*)newVals
[_tempDomains setObject: regDefs forKey: NSRegistrationDomain];
}
DESTROY(_dictionaryRep);
defaultsDidChange();
[regDefs addEntriesFromDictionary: newVals];
updateCache(self);
[_lock unlock];
Expand All @@ -2303,6 +2416,7 @@ - (void) removeSuiteNamed: (NSString*)aName
NS_DURING
{
DESTROY(_dictionaryRep);
defaultsDidChange();
[_searchList removeObject: aName];
updateCache(self);
[_lock unlock];
Expand Down Expand Up @@ -2537,6 +2651,7 @@ - (void) _changePersistentDomain: (NSString*)domainName
NS_DURING
{
DESTROY(_dictionaryRep);
defaultsDidChange();
if (_changedDomains == nil)
{
_changedDomains = [[NSMutableArray alloc] initWithObjects: &domainName
Expand Down Expand Up @@ -2863,6 +2978,7 @@ - (BOOL) setObject: (id)anObject forKey: (NSString*)aKey
[removed addObject: aKey];
}
[contents removeObjectForKey: aKey];
defaultsDidChange();
return YES;
}
else
Expand Down Expand Up @@ -2890,6 +3006,7 @@ - (BOOL) setObject: (id)anObject forKey: (NSString*)aKey
}
}
[contents setObject: anObject forKey: aKey];
defaultsDidChange();
return YES;
}
}
Expand Down Expand Up @@ -3007,6 +3124,7 @@ - (BOOL) synchronize
}
}
ASSIGN(contents, disk);
defaultsDidChange();
}
if (YES == hasLocalChanges)
{
Expand Down
Loading