Huge set of performance improvements, combined with an architecture specific build and clang-cl users have reported absurd gains over master for some gains, in the range 50%-90%

But for normal msvc builds i would put it at around 30-50%
Added per-xexmodule caching of information per instruction, can be used to remember what code needs compiling at start up
Record what guest addresses wrote mmio and backpropagate that to future runs, eliminating dependence on exception trapping. this makes many games like h3 actually tolerable to run under a debugger
fixed a number of errors where temporaries were being passed by reference/pointer
Can now be compiled with clang-cl 14.0.1, requires -Werror off though and some other solution/project changes.
Added macros wrapping compiler extensions like noinline, forceinline, __expect, and cold.
Removed the "global lock" in guest code completely. It does not properly emulate the behavior of mfmsrd/mtmsr and it seriously cripples amd cpus. Removing this yielded around a 3x speedup in Halo Reach for me.
Disabled the microprofiler for now. The microprofiler has a huge performance cost associated with it. Developers can re-enable it in the base/profiling header if they really need it
Disable the trace writer in release builds. despite just returning after checking if the file was open the trace functions were consuming about 0.60% cpu time total
Add IsValidReg, GetRegisterInfo is a huge (about 45k) branching function and using that to check if a register was valid consumed a significant chunk of time
Optimized RingBuffer::ReadAndSwap and RingBuffer::read_count. This gave us the largest overall boost in performance. The memcpies were unnecessary and one of them was always a no-op
Added simplification rules for multiplicative patterns like (x+x), (x<<1)+x
For the most frequently called win32 functions i added code to call their underlying NT implementations, which lets us skip a lot of MS code we don't care about/isnt relevant to our usecases
^this can be toggled off in the platform_win header
handle indirect call true with constant function pointer, was occurring in h3
lookup host format swizzle in denser array
by default, don't check if a gpu register is unknown, instead just check if its out of range. controlled by a cvar
^looking up whether its known or not took approx 0.3% cpu time
Changed some things in /cpu to make the project UNITYBUILD friendly
The timer thread was spinning way too much and consuming a ton of cpu, changed it to use a blocking wait instead
tagged some conditions as XE_UNLIKELY/LIKELY based on profiler feedback (will only affect clang builds)
Shifted around some code in CommandProcessor::WriteRegister based on how frequently it was executed
added support for docdecaduple precision floating point so that we can represent our performance gains numerically
tons of other stuff im probably forgetting
This commit is contained in:
chss95cs@gmail.com
2022-08-13 12:59:00 -07:00
parent 2f59487bf3
commit cb85fe401c
49 changed files with 1462 additions and 483 deletions

View File

@@ -46,7 +46,9 @@ static_assert((std::endian::native == std::endian::big) ||
namespace xe {
#if XE_COMPILER_MSVC
// chrispy: added workaround for clang, otherwise byteswap_ulong becomes calls
// to ucrtbase
#if XE_COMPILER_MSVC == 1 && !defined(__clang__)
#define XENIA_BASE_BYTE_SWAP_16 _byteswap_ushort
#define XENIA_BASE_BYTE_SWAP_32 _byteswap_ulong
#define XENIA_BASE_BYTE_SWAP_64 _byteswap_uint64

View File

@@ -28,7 +28,8 @@ namespace xe {
class Win32MappedMemory : public MappedMemory {
public:
// CreateFile returns INVALID_HANDLE_VALUE in case of failure.
static constexpr HANDLE kFileHandleInvalid = INVALID_HANDLE_VALUE;
// chrispy: made inline const to get around clang error
static inline const HANDLE kFileHandleInvalid = INVALID_HANDLE_VALUE;
// CreateFileMapping returns nullptr in case of failure.
static constexpr HANDLE kMappingHandleInvalid = nullptr;

View File

@@ -15,7 +15,15 @@
WINAPI_PARTITION_SYSTEM | WINAPI_PARTITION_GAMES)
#define XE_BASE_MEMORY_WIN_USE_DESKTOP_FUNCTIONS
#endif
/*
these two dont bypass much ms garbage compared to the threading ones,
but Protect is used by PhysicalHeap::EnableAccessCallbacks which eats a lot
of cpu time, so every bit counts
*/
XE_NTDLL_IMPORT(NtProtectVirtualMemory, cls_NtProtectVirtualMemory,
NtProtectVirtualMemoryPointer);
XE_NTDLL_IMPORT(NtQueryVirtualMemory, cls_NtQueryVirtualMemory,
NtQueryVirtualMemoryPointer);
namespace xe {
namespace memory {
@@ -139,6 +147,18 @@ bool Protect(void* base_address, size_t length, PageAccess access,
*out_old_access = PageAccess::kNoAccess;
}
DWORD new_protect = ToWin32ProtectFlags(access);
#if XE_USE_NTDLL_FUNCTIONS == 1
DWORD old_protect = 0;
SIZE_T MemoryLength = length;
PVOID MemoryCache = base_address;
BOOL result = NtProtectVirtualMemoryPointer.invoke<NTSTATUS>(
(HANDLE)0xFFFFFFFFFFFFFFFFLL, &MemoryCache, &MemoryLength,
new_protect, &old_protect) >= 0;
#else
#ifdef XE_BASE_MEMORY_WIN_USE_DESKTOP_FUNCTIONS
DWORD old_protect = 0;
BOOL result = VirtualProtect(base_address, length, new_protect, &old_protect);
@@ -146,6 +166,7 @@ bool Protect(void* base_address, size_t length, PageAccess access,
ULONG old_protect = 0;
BOOL result = VirtualProtectFromApp(base_address, length, ULONG(new_protect),
&old_protect);
#endif
#endif
if (!result) {
return false;
@@ -161,8 +182,17 @@ bool QueryProtect(void* base_address, size_t& length, PageAccess& access_out) {
MEMORY_BASIC_INFORMATION info;
ZeroMemory(&info, sizeof(info));
#if XE_USE_NTDLL_FUNCTIONS == 1
ULONG_PTR ResultLength;
NTSTATUS query_result = NtQueryVirtualMemoryPointer.invoke<NTSTATUS>(
(HANDLE)0xFFFFFFFFFFFFFFFFLL, (PVOID)base_address,
0 /* MemoryBasicInformation*/, &info, length, &ResultLength);
SIZE_T result = query_result >= 0 ? ResultLength : 0;
#else
SIZE_T result = VirtualQuery(base_address, &info, length);
#endif
if (!result) {
return false;
}

View File

@@ -10,10 +10,9 @@
#include "xenia/base/mutex.h"
namespace xe {
std::recursive_mutex& global_critical_region::mutex() {
static std::recursive_mutex global_mutex;
return global_mutex;
}
// chrispy: moved this out of body of function to eliminate the initialization
// guards
static std::recursive_mutex global_mutex;
std::recursive_mutex& global_critical_region::mutex() { return global_mutex; }
} // namespace xe

View File

@@ -41,19 +41,33 @@
#error Unsupported target OS.
#endif
#if defined(__clang__)
#if defined(__clang__) && !defined(_MSC_VER) // chrispy: support clang-cl
#define XE_COMPILER_CLANG 1
#define XE_COMPILER_HAS_CLANG_EXTENSIONS 1
#elif defined(__GNUC__)
#define XE_COMPILER_GNUC 1
#define XE_COMPILER_HAS_GNU_EXTENSIONS 1
#elif defined(_MSC_VER)
#define XE_COMPILER_MSVC 1
#define XE_COMPILER_HAS_MSVC_EXTENSIONS 1
#elif defined(__MINGW32)
#define XE_COMPILER_MINGW32 1
#define XE_COMPILER_HAS_GNU_EXTENSIONS 1
#elif defined(__INTEL_COMPILER)
#define XE_COMPILER_INTEL 1
#else
#define XE_COMPILER_UNKNOWN 1
#endif
// chrispy: had to place this here.
#if defined(__clang__) && defined(_MSC_VER)
#define XE_COMPILER_CLANG_CL 1
#define XE_COMPILER_HAS_CLANG_EXTENSIONS 1
#endif
// clang extensions == superset of gnu extensions
#if XE_COMPILER_HAS_CLANG_EXTENSIONS == 1
#define XE_COMPILER_HAS_GNU_EXTENSIONS 1
#endif
#if defined(_M_AMD64) || defined(__amd64__)
#define XE_ARCH_AMD64 1
@@ -93,6 +107,29 @@
#define XEPACKEDSTRUCTANONYMOUS(value) _XEPACKEDSCOPE(struct value)
#define XEPACKEDUNION(name, value) _XEPACKEDSCOPE(union name value)
#if XE_COMPILER_HAS_MSVC_EXTENSIONS == 1
#define XE_FORCEINLINE __forceinline
#define XE_NOINLINE __declspec(noinline)
// can't properly emulate "cold" in msvc, but can still segregate the function
// into its own seg
#define XE_COLD __declspec(code_seg(".cold"))
#define XE_LIKELY(...) (!!(__VA_ARGS__))
#define XE_UNLIKELY(...) (!!(__VA_ARGS__))
#elif XE_COMPILER_HAS_GNU_EXTENSIONS == 1
#define XE_FORCEINLINE __attribute__((always_inline))
#define XE_NOINLINE __attribute__((noinline))
#define XE_COLD __attribute__((cold))
#define XE_LIKELY(...) __builtin_expect(!!(__VA_ARGS__), true)
#define XE_UNLIKELY(...) __builtin_expect(!!(__VA_ARGS__), false)
#else
#define XE_FORCEINLINE inline
#define XE_NOINLINE
#define XE_COLD
#define XE_LIKELY(...) (!!(__VA_ARGS__))
#define XE_UNLIKELY(...) (!!(__VA_ARGS__))
#endif
namespace xe {
#if XE_PLATFORM_WIN32

View File

@@ -34,4 +34,31 @@
#undef DeleteFile
#undef GetFirstChild
#define XE_USE_NTDLL_FUNCTIONS 1
#if XE_USE_NTDLL_FUNCTIONS==1
/*
ntdll versions of functions often skip through a lot of extra garbage in KernelBase
*/
#define XE_NTDLL_IMPORT(name, cls, clsvar) \
static class cls { \
public: \
FARPROC fn;\
cls() : fn(nullptr) {\
auto ntdll = GetModuleHandleA("ntdll.dll");\
if (ntdll) { \
fn = GetProcAddress(ntdll, #name );\
}\
} \
template <typename TRet = void, typename... TArgs> \
inline TRet invoke(TArgs... args) {\
return reinterpret_cast<NTSYSAPI TRet(NTAPI*)(TArgs...)>(fn)(args...);\
}\
inline operator bool() const {\
return fn!=nullptr;\
}\
} clsvar
#else
#define XE_NTDLL_IMPORT(name, cls, clsvar) static constexpr bool clsvar = false
#endif
#endif // XENIA_BASE_PLATFORM_WIN_H_

View File

@@ -20,7 +20,7 @@
#include "xenia/ui/virtual_key.h"
#include "xenia/ui/window_listener.h"
#if XE_PLATFORM_WIN32
#if XE_PLATFORM_WIN32 && 0
#define XE_OPTION_PROFILING 1
#define XE_OPTION_PROFILING_UI 1
#else

View File

@@ -19,7 +19,26 @@
#include "xenia/base/byte_order.h"
namespace xe {
/*
todo: this class is CRITICAL to the performance of the entire emulator
currently, about 0.74% cpu time is still taken up by ReadAndSwap, 0.23
is used by read_count I believe that part of the issue is that smaller
ringbuffers are kicking off an automatic prefetcher stream, that ends up
reading ahead of the end of the ring because it can only go in a straight
line it then gets a cache miss when it eventually wraps around to the start
of the ring? really hard to tell whats going on there honestly, maybe we can
occasionally prefetch the first line of the ring to L1? For the automatic
prefetching i don't think there are any good options. I don't know if we have
any control over where these buffers will be (they seem to be in guest memory
:/), but if we did we could right-justify the buffer so that the final byte
of the ring ends at the end of a page. i think most automatic prefetchers
cannot cross page boundaries it does feel like something isnt right here
though
todo: microoptimization, we can change our size members to be uint32 so
that the registers no longer need the rex prefix, shrinking the generated
code a bit.. like i said, every bit helps in this class
*/
class RingBuffer {
public:
RingBuffer(uint8_t* buffer, size_t capacity);
@@ -32,6 +51,8 @@ class RingBuffer {
uintptr_t read_ptr() const { return uintptr_t(buffer_) + read_offset_; }
void set_read_offset(size_t offset) { read_offset_ = offset % capacity_; }
size_t read_count() const {
// chrispy: these branches are unpredictable
#if 0
if (read_offset_ == write_offset_) {
return 0;
} else if (read_offset_ < write_offset_) {
@@ -39,6 +60,33 @@ class RingBuffer {
} else {
return (capacity_ - read_offset_) + write_offset_;
}
#else
size_t read_offs = read_offset_;
size_t write_offs = write_offset_;
size_t cap = capacity_;
size_t offset_delta = write_offs - read_offs;
size_t wrap_read_count = (cap - read_offs) + write_offs;
size_t comparison_value = read_offs <= write_offs;
#if 0
size_t selector =
static_cast<size_t>(-static_cast<ptrdiff_t>(comparison_value));
offset_delta &= selector;
wrap_read_count &= ~selector;
return offset_delta | wrap_read_count;
#else
if (XE_LIKELY(read_offs <= write_offs)) {
return offset_delta; // will be 0 if they are equal, semantically
// identical to old code (i checked the asm, msvc
// does not automatically do this)
} else {
return wrap_read_count;
}
#endif
#endif
}
size_t write_offset() const { return write_offset_; }
@@ -113,6 +161,28 @@ class RingBuffer {
size_t write_offset_ = 0;
};
template <>
inline uint32_t RingBuffer::ReadAndSwap<uint32_t>() {
size_t read_offset = this->read_offset_;
xenia_assert(this->capacity_ >= 4);
size_t next_read_offset = read_offset + 4;
#if 0
size_t zerotest = next_read_offset - this->capacity_;
// unpredictable branch, use bit arith instead
// todo: it would be faster to use lzcnt, but we need to figure out if all
// machines we support support it
next_read_offset &= -static_cast<ptrdiff_t>(!!zerotest);
#else
if (XE_UNLIKELY(next_read_offset == this->capacity_)) {
next_read_offset = 0;
//todo: maybe prefetch next? or should that happen much earlier?
}
#endif
this->read_offset_ = next_read_offset;
unsigned int ring_value = *(uint32_t*)&this->buffer_[read_offset];
return xe::byte_swap(ring_value);
}
} // namespace xe
#endif // XENIA_BASE_RING_BUFFER_H_

View File

@@ -10,12 +10,12 @@
#include <algorithm>
#include <forward_list>
#include "third_party/disruptorplus/include/disruptorplus/blocking_wait_strategy.hpp"
#include "third_party/disruptorplus/include/disruptorplus/multi_threaded_claim_strategy.hpp"
#include "third_party/disruptorplus/include/disruptorplus/ring_buffer.hpp"
#include "third_party/disruptorplus/include/disruptorplus/sequence_barrier.hpp"
#include "third_party/disruptorplus/include/disruptorplus/spin_wait.hpp"
#include "third_party/disruptorplus/include/disruptorplus/spin_wait_strategy.hpp"
#include "xenia/base/assert.h"
#include "xenia/base/threading.h"
#include "xenia/base/threading_timer_queue.h"
@@ -26,6 +26,12 @@ namespace xe {
namespace threading {
using WaitItem = TimerQueueWaitItem;
/*
chrispy: changed this to a blocking wait from a spin-wait, the spin was
monopolizing a ton of cpu time (depending on the game 2-4% of total cpu time)
on my 3990x no complaints since that change
*/
using WaitStrat = dp::blocking_wait_strategy;
class TimerQueue {
public:
@@ -147,9 +153,10 @@ class TimerQueue {
// This ring buffer will be used to introduce timers queued by the public API
static constexpr size_t kWaitCount = 512;
dp::ring_buffer<std::shared_ptr<WaitItem>> buffer_;
dp::spin_wait_strategy wait_strategy_;
dp::multi_threaded_claim_strategy<dp::spin_wait_strategy> claim_strategy_;
dp::sequence_barrier<dp::spin_wait_strategy> consumed_;
WaitStrat wait_strategy_;
dp::multi_threaded_claim_strategy<WaitStrat> claim_strategy_;
dp::sequence_barrier<WaitStrat> consumed_;
// This is a _sorted_ (ascending due_) list of active timers managed by a
// dedicated thread

View File

@@ -7,19 +7,49 @@
******************************************************************************
*/
#include <winternl.h>
#include "xenia/base/assert.h"
#include "xenia/base/chrono_steady_cast.h"
#include "xenia/base/logging.h"
#include "xenia/base/platform_win.h"
#include "xenia/base/threading.h"
#include "xenia/base/threading_timer_queue.h"
#define LOG_LASTERROR() \
{ XELOGI("Win32 Error 0x{:08X} in " __FUNCTION__ "(...)", GetLastError()); }
#if defined(__clang__)
// chrispy: i do not understand why this is an error for clang here
// something about the quoted __FUNCTION__ freaks it out (clang 14.0.1)
#define LOG_LASTERROR() \
do { \
XELOGI("Win32 Error 0x{:08X} in {} (...)", GetLastError(), __FUNCTION__); \
} while (false)
#else
#define LOG_LASTERROR() \
do { \
XELOGI("Win32 Error 0x{:08X} in " __FUNCTION__ "(...)", GetLastError()); \
} while (false)
#endif
typedef HANDLE (*SetThreadDescriptionFn)(HANDLE hThread,
PCWSTR lpThreadDescription);
// sys function for ntyieldexecution, by calling it we sidestep
// RtlGetCurrentUmsThread
XE_NTDLL_IMPORT(NtYieldExecution, cls_NtYieldExecution,
NtYieldExecutionPointer);
// sidestep the activation context/remapping special windows handles like stdout
XE_NTDLL_IMPORT(NtWaitForSingleObject, cls_NtWaitForSingleObject,
NtWaitForSingleObjectPointer);
XE_NTDLL_IMPORT(NtSetEvent, cls_NtSetEvent, NtSetEventPointer);
// difference between NtClearEvent and NtResetEvent is that NtResetEvent returns
// the events state prior to the call, but we dont need that. might need to
// check whether one or the other is faster in the kernel though yeah, just
// checked, the code in ntoskrnl is way simpler for clearevent than resetevent
XE_NTDLL_IMPORT(NtClearEvent, cls_NtClearEvent, NtClearEventPointer);
XE_NTDLL_IMPORT(NtPulseEvent, cls_NtPulseEvent, NtPulseEventPointer);
// heavily called, we dont skip much garbage by calling this, but every bit
// counts
XE_NTDLL_IMPORT(NtReleaseSemaphore, cls_NtReleaseSemaphore,
NtReleaseSemaphorePointer);
namespace xe {
namespace threading {
@@ -80,7 +110,13 @@ void set_name(const std::string_view name) {
}
void MaybeYield() {
#if defined(XE_USE_NTDLL_FUNCTIONS)
NtYieldExecutionPointer.invoke();
#else
SwitchToThread();
#endif
// memorybarrier is really not necessary here...
MemoryBarrier();
}
@@ -134,8 +170,26 @@ class Win32Handle : public T {
WaitResult Wait(WaitHandle* wait_handle, bool is_alertable,
std::chrono::milliseconds timeout) {
HANDLE handle = wait_handle->native_handle();
DWORD result = WaitForSingleObjectEx(handle, DWORD(timeout.count()),
is_alertable ? TRUE : FALSE);
DWORD result;
DWORD timeout_dw = DWORD(timeout.count());
BOOL bAlertable = is_alertable ? TRUE : FALSE;
// todo: we might actually be able to use NtWaitForSingleObject even if its
// alertable, just need to study whether
// RtlDeactivateActivationContextUnsafeFast/RtlActivateActivationContext are
// actually needed for us
#if XE_USE_NTDLL_FUNCTIONS == 1
if (bAlertable) {
result = WaitForSingleObjectEx(handle, timeout_dw, bAlertable);
} else {
LARGE_INTEGER timeout_big;
timeout_big.QuadPart = -10000LL * static_cast<int64_t>(timeout_dw);
result = NtWaitForSingleObjectPointer.invoke<NTSTATUS>(
handle, bAlertable, timeout_dw == INFINITE ? nullptr : &timeout_big);
}
#else
result = WaitForSingleObjectEx(handle, timeout_dw, bAlertable);
#endif
switch (result) {
case WAIT_OBJECT_0:
return WaitResult::kSuccess;
@@ -178,7 +232,9 @@ std::pair<WaitResult, size_t> WaitMultiple(WaitHandle* wait_handles[],
size_t wait_handle_count,
bool wait_all, bool is_alertable,
std::chrono::milliseconds timeout) {
std::vector<HANDLE> handles(wait_handle_count);
std::vector<HANDLE> handles(
wait_handle_count); // max handles is like 64, so it would make more
// sense to just do a fixed size array here
for (size_t i = 0; i < wait_handle_count; ++i) {
handles[i] = wait_handles[i]->native_handle();
}
@@ -208,9 +264,16 @@ class Win32Event : public Win32Handle<Event> {
public:
explicit Win32Event(HANDLE handle) : Win32Handle(handle) {}
~Win32Event() override = default;
#if XE_USE_NTDLL_FUNCTIONS == 1
void Set() override { NtSetEventPointer.invoke(handle_, nullptr); }
void Reset() override { NtClearEventPointer.invoke(handle_); }
void Pulse() override { NtPulseEventPointer.invoke(handle_, nullptr); }
#else
void Set() override { SetEvent(handle_); }
void Reset() override { ResetEvent(handle_); }
void Pulse() override { PulseEvent(handle_); }
#endif
};
std::unique_ptr<Event> Event::CreateManualResetEvent(bool initial_state) {
@@ -220,6 +283,7 @@ std::unique_ptr<Event> Event::CreateManualResetEvent(bool initial_state) {
return std::make_unique<Win32Event>(handle);
} else {
LOG_LASTERROR();
return nullptr;
}
}
@@ -240,10 +304,15 @@ class Win32Semaphore : public Win32Handle<Semaphore> {
explicit Win32Semaphore(HANDLE handle) : Win32Handle(handle) {}
~Win32Semaphore() override = default;
bool Release(int release_count, int* out_previous_count) override {
#if XE_USE_NTDLL_FUNCTIONS == 1
return NtReleaseSemaphorePointer.invoke<NTSTATUS>(handle_, release_count,
out_previous_count) >= 0;
#else
return ReleaseSemaphore(handle_, release_count,
reinterpret_cast<LPLONG>(out_previous_count))
? true
: false;
#endif
}
};

View File

@@ -82,8 +82,9 @@ std::string upper_ascii(const std::string_view view) {
template <bool LOWER>
inline size_t hash_fnv1a(const std::string_view view) {
const size_t offset_basis = 0xCBF29CE484222325ull;
const size_t prime = 0x00000100000001B3ull;
auto work = [&prime](size_t hash, uint8_t byte_of_data) {
// chrispy: constant capture errors on clang
auto work = [](size_t hash, uint8_t byte_of_data) {
const size_t prime = 0x00000100000001B3ull;
hash ^= byte_of_data;
hash *= prime;
return hash;