Skip to content

perf(String): Remove global locks on reference counters and replace them with atomic variables in string classes - #3495

Open
Mauller wants to merge 2 commits into
TheSuperHackers:mainfrom
Mauller:Mauller/perf-remove-string-class-global-locks
Open

Mauller wants to merge 2 commits into
TheSuperHackers:mainfrom
Mauller:Mauller/perf-remove-string-class-global-locks

Conversation

@Mauller

@Mauller Mauller commented Oct 12, 2026

Copy link
Copy Markdown

Merge with rebase


This PR removes the critical section global locks from the AsciiString and UnicodeString classes.

In their place we use atomic counters for managing the reference counters.

@Mauller Mauller self-assigned this Oct 12, 2026
@Mauller Mauller added Major Severity: Minor < Major < Critical < Blocker Performance Is a performance concern Gen Relates to Generals ZH Relates to Zero Hour Memory Is memory related labels Oct 12, 2026
@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5adf0ebb-3839-493c-8ed5-ee84a953ab23

📥 Commits

Reviewing files that changed from the base of the PR and between 2201b8c and 7ab9891.


📒 Files selected for processing (1)
  • GeneralsMD/Code/Main/WinMain.cpp

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.



Walkthrough

ASCII and Unicode string buffer reference counts now use std::atomic<short>. String copy, release, and assignment operations no longer acquire their dedicated critical sections. The declarations, definitions, startup setup, and benchmark setup for those critical sections were also removed.

Changes

String reference-count synchronization

Layer / File(s) Summary
Atomic string buffer reference counts
Core/GameEngine/Include/Common/AsciiString.h, Core/GameEngine/Include/Common/UnicodeString.h, Core/GameEngine/Source/Common/System/AsciiString.cpp, Core/GameEngine/Source/Common/System/UnicodeString.cpp
The string data structures use atomic reference-count fields. Copy, release, and assignment operations no longer acquire string critical-section guards.
Remove string critical-section setup
GeneralsMD/Code/GameEngine/Include/Common/CriticalSection.h, GeneralsMD/Code/GameEngine/Source/Common/System/CriticalSection.cpp, GeneralsMD/Code/Main/WinMain.cpp, Benchmarks/Google/Core/GoogleBenchmarkMain.cpp
The ASCII and Unicode critical-section declarations and definitions were removed. Startup and benchmark setup now manage only the DMA, memory-pool, and debug-log critical sections.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Refactor

Suggested reviewers: xezon

Merge Risk: 🟡 Moderate · up to 7ab98

The supported VC6 retail build uses ASCII and Unicode string buffers whose atomic reference counts are not constructed before use. Resolve this core string-lifetime risk before merging.

Pre-merge checks · Passed 4
Passed Passed checks (4 passed)
Check name Status Explanation
Title check Passed The title clearly states the main change: replacing global string reference-counter locks with atomic variables.
Description check Passed The description directly explains the removal of critical-section locks and the use of atomic reference counters in both string classes.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Mauller
Mauller force-pushed the Mauller/perf-remove-string-class-global-locks branch from 4ac97b0 to 3d8efcf Compare October 12, 2026 16:56
Comment on lines 252 to 254
{
ScopedCriticalSection scopedCriticalSection(TheAsciiStringCriticalSection);

validate();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High System/AsciiString.cpp:252

Concurrent assignment can increment the reference count of a buffer after another thread has freed it, causing a use-after-free and corrupting ownership. set reads stringSrc.m_data and increments its reference count without the critical-section guard; restore the guard around this ownership handoff.

Suggested change
validate();
ScopedCriticalSection scopedCriticalSection(TheAsciiStringCriticalSection);
validate();
Also found in 1 other location(s)

Core/GameEngine/Source/Common/System/UnicodeString.cpp:69

Removing the lock from UnicodeString copy construction leaves copy-on-write mutation unsynchronized. ensureUniqueBufferOfSize() checks m_refCount == 1 and then writes into the shared buffer; another thread can execute this constructor between that check and the write and increment the atomic count, so the mutating thread modifies storage now owned by both strings. Concurrent readers/writers can therefore observe corrupted string contents. Making only the counter atomic does not make the check-then-mutate sequence safe.

🤖 Copy this AI Prompt to have your agent fix this:
In file @Core/GameEngine/Source/Common/System/AsciiString.cpp around line 252:

Concurrent assignment can increment the reference count of a buffer after another thread has freed it, causing a use-after-free and corrupting ownership. `set` reads `stringSrc.m_data` and increments its reference count without the critical-section guard; restore the guard around this ownership handoff.

Also found in 1 other location(s):
- Core/GameEngine/Source/Common/System/UnicodeString.cpp:69 -- Removing the lock from `UnicodeString` copy construction leaves copy-on-write mutation unsynchronized. `ensureUniqueBufferOfSize()` checks `m_refCount == 1` and then writes into the shared buffer; another thread can execute this constructor between that check and the write and increment the atomic count, so the mutating thread modifies storage now owned by both strings. Concurrent readers/writers can therefore observe corrupted string contents. Making only the counter atomic does not make the check-then-mutate sequence safe.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

fixed

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This remains unprotected: set increments stringSrc.m_data's refcount without synchronization, so concurrent destruction/assignment of stringSrc can still free it first.

@greptile-apps

greptile-apps Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[High impact] The PR appears safe to merge; no actionable issue remains.

Summary

The PR replaces global string locks with atomic reference counters in AsciiString and UnicodeString.

  • String copies share data without global string locks.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A["set(source)"] --> B["Read source buffer"]
  B --> C["Increase incoming reference count"]
  C --> D["Release old buffer"]
  D --> E{"Old count reaches zero?"}
  E -->|Yes| F["Destroy header and free storage"]
  E -->|No| G["Keep shared storage"]
  F --> H["Store incoming buffer"]
  G --> H
Loading

Reviews (5) · Last reviewed commit: "perf(UnicodeString): Remove global lock ..." · Reviewed by Greptile

Comment thread GeneralsMD/Code/GameEngine/Include/Common/CriticalSection.h
Comment thread Core/GameEngine/Include/Common/AsciiString.h
Comment thread Core/GameEngine/Include/Common/AsciiString.h Outdated
Comment thread GeneralsMD/Code/Main/WinMain.cpp Outdated
Comment thread GeneralsMD/Code/Main/WinMain.cpp Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟠 Major · Restore the string critical sections. · AsciiString.cpp:250-260

Core/GameEngine/Source/Common/System/AsciiString.cpp:250-260
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Restore the string critical sections.

set reads stringSrc.m_data before incrementing its atomic reference count. If another thread calls clear() on the same source, releaseBuffer() can free the buffer between those operations. The atomic counter does not protect this pointer acquisition, so the increment can access freed memory.

Restore the critical sections in both set and releaseBuffer for both string types.

Suggested fix
+#include "Common/CriticalSection.h"
 #include "WWLib/utf8.h"
@@
 void AsciiString::releaseBuffer()
 {
+	ScopedCriticalSection scopedCriticalSection(TheAsciiStringCriticalSection);
 	validate();
@@
 void AsciiString::set(const AsciiString& stringSrc)
 {
+	ScopedCriticalSection scopedCriticalSection(TheAsciiStringCriticalSection);
 	validate();
+#include "Common/CriticalSection.h"
 #include "WWLib/utf8.h"
@@
 void UnicodeString::releaseBuffer()
 {
+	ScopedCriticalSection scopedCriticalSection(TheUnicodeStringCriticalSection);
 	validate();
@@
 void UnicodeString::set(const UnicodeString& stringSrc)
 {
+	ScopedCriticalSection scopedCriticalSection(TheUnicodeStringCriticalSection);
 	validate();

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: be89b0e9-ebdb-4436-9965-319f2af367b8
📥 Commits

Reviewing files that changed from the base of the PR and between d1bd190 and 3d8efcf.

📒 Files selected for processing (8)
  • Benchmarks/Google/Core/GoogleBenchmarkMain.cpp
  • Core/GameEngine/Include/Common/AsciiString.h
  • Core/GameEngine/Include/Common/UnicodeString.h
  • Core/GameEngine/Source/Common/System/AsciiString.cpp
  • Core/GameEngine/Source/Common/System/UnicodeString.cpp
  • GeneralsMD/Code/GameEngine/Include/Common/CriticalSection.h
  • GeneralsMD/Code/GameEngine/Source/Common/System/CriticalSection.cpp
  • GeneralsMD/Code/Main/WinMain.cpp
💤 Files with no reviewable changes (5)
  • GeneralsMD/Code/GameEngine/Include/Common/CriticalSection.h
  • Core/GameEngine/Source/Common/System/UnicodeString.cpp
  • Core/GameEngine/Source/Common/System/AsciiString.cpp
  • GeneralsMD/Code/GameEngine/Source/Common/System/CriticalSection.cpp
  • Benchmarks/Google/Core/GoogleBenchmarkMain.cpp

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.

Comment thread Core/GameEngine/Include/Common/AsciiString.h
@Mauller
Mauller force-pushed the Mauller/perf-remove-string-class-global-locks branch from 3d8efcf to 2201b8c Compare October 12, 2026 17:14

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟠 Major · Acquire the source buffer before releasing the destination buffer. · AsciiString.cpp:250-260

Core/GameEngine/Source/Common/System/AsciiString.cpp:250-260
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Acquire the source buffer before releasing the destination buffer.

When two distinct AsciiString instances share one buffer and call set on each other concurrently, both setters can decrement the shared reference count before either increments it. One setter can free the buffer, after which the other setter increments m_refCount through the freed pointer. UnicodeString::set has the same ownership-transfer sequence and failure.

Suggested fix
 void AsciiString::set(const AsciiString& stringSrc)
 {
 	validate();
 	if (&stringSrc != this)
 	{
+		AsciiStringData* newData = stringSrc.m_data;
+		if (newData)
+			++newData->m_refCount;
 		releaseBuffer();
-		m_data = stringSrc.m_data;
-		if (m_data)
-			++m_data->m_refCount;
+		m_data = newData;
 	}
 	validate();
 }
 void UnicodeString::set(const UnicodeString& stringSrc)
 {
 	validate();
 	if (&stringSrc != this)
 	{
+		UnicodeStringData* newData = stringSrc.m_data;
+		if (newData)
+			++newData->m_refCount;
 		releaseBuffer();
-		m_data = stringSrc.m_data;
-		if (m_data)
-			++m_data->m_refCount;
+		m_data = newData;
 	}
 	validate();
 }

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8b549ab7-9cb7-4dee-b934-aea7464c230c
📥 Commits

Reviewing files that changed from the base of the PR and between 3d8efcf and 2201b8c.

📒 Files selected for processing (2)
  • Core/GameEngine/Include/Common/AsciiString.h
  • Core/GameEngine/Include/Common/UnicodeString.h

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.

@Mauller
Mauller force-pushed the Mauller/perf-remove-string-class-global-locks branch 2 times, most recently from 7ab9891 to 74a69ed Compare October 12, 2026 20:21
@xezon

xezon commented Oct 12, 2026

Copy link
Copy Markdown

The bots still have a complaint about atomic initialization

@Mauller

Mauller commented Oct 12, 2026

Copy link
Copy Markdown
Author

The bots still have a complaint about atomic initialization

Just going through them, one looks like it was already an inherent problem, but i think it becomes more of an issue with the change to atomic variables.

@Mauller
Mauller force-pushed the Mauller/perf-remove-string-class-global-locks branch from 74a69ed to 7ff8844 Compare October 12, 2026 21:23
Comment on lines 219 to 224
if (--m_data->m_refCount == 0)
{
m_data->~AsciiStringData();
TheDynamicMemoryAllocator->freeBytes(m_data);
}
m_data = nullptr;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is this safe? Imagine the following scenario:

Thread_1:

//MAYBE_UNUSED AsciiString str1("test");

if (str1.m_data)
{
	if (--str1.m_data->m_refCount == 0)
	{
		str1.m_data->~AsciiStringData();
		TheDynamicMemoryAllocator->freeBytes(str1.m_data);

		// <-- thread_1 is here
	}
	str1.m_data = nullptr;
}

Thread_2:

// thread_2 holds a reference to `str` and executes this before thread_1 can reset the data pointer
MAYBE_UNUSED AsciiString str2 = std::ref(str1);

Thread_2 would access the data m_data points to, even though it's already freed, so use-after-free.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yes this looks like a race. Needs to null str1.m_data first and cache it in a local var before touching the reference counter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Gen Relates to Generals Major Severity: Minor < Major < Critical < Blocker Memory Is memory related Performance Is a performance concern ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants