diff --git a/src/common/threading.cpp b/src/common/threading.cpp
index 62c8bfa7b..ac272ff1c 100644
--- a/src/common/threading.cpp
+++ b/src/common/threading.cpp
@@ -484,7 +484,7 @@ u64 Threading::GetThreadTicksPerSecond()
   // On x86, despite what the MS documentation says, this basically appears to be rdtsc.
   // So, the frequency is our base clock speed (and stable regardless of power management).
   static u64 frequency = 0;
-  if (UNLIKELY(frequency == 0))
+  if (frequency == 0) [[unlikely]]
   {
     frequency = 1000000;
 
diff --git a/src/common/types.h b/src/common/types.h
index 0d0d11eb7..409acdb7f 100644
--- a/src/common/types.h
+++ b/src/common/types.h
@@ -56,15 +56,6 @@ char (&__countof_ArraySizeHelper(T (&array)[N]))[N];
 #define printflike(n,m)
 #endif
 
-#ifdef _MSC_VER
-// TODO: Use C++20 [[likely]] when available.
-#define LIKELY(x)  (!!(x))
-#define UNLIKELY(x)  (!!(x))
-#else
-#define LIKELY(x) __builtin_expect(!!(x), 1)
-#define UNLIKELY(x) __builtin_expect(!!(x), 0)
-#endif
-
 // [[noreturn]] which can be used on function pointers.
 #ifdef _MSC_VER
 // __declspec(noreturn) produces error C3829.
diff --git a/src/core/cpu_core.cpp b/src/core/cpu_core.cpp
index 4dcf742f7..4f4386e07 100644
--- a/src/core/cpu_core.cpp
+++ b/src/core/cpu_core.cpp
@@ -2125,9 +2125,9 @@ template<PGXPMode pgxp_mode, bool debug>
         if (s_trace_to_log)
           LogInstruction(g_state.current_instruction.bits, g_state.current_instruction_pc, &g_state.regs);
 
-        if (UNLIKELY(g_state.current_instruction_pc == 0xA0))
+        if (g_state.current_instruction_pc == 0xA0) [[unlikely]]
           HandleA0Syscall();
-        else if (UNLIKELY(g_state.current_instruction_pc == 0xB0))
+        else if (g_state.current_instruction_pc == 0xB0) [[unlikely]]
           HandleB0Syscall();
       }
 
diff --git a/src/core/mdec.cpp b/src/core/mdec.cpp
index b3ff4f541..dc0a56f0c 100644
--- a/src/core/mdec.cpp
+++ b/src/core/mdec.cpp
@@ -634,7 +634,7 @@ void MDEC::CopyOutBlock(void* param, TickCount ticks, TickCount ticks_late)
 
     case DataOutputDepth_15Bit:
     {
-      if (UNLIKELY(g_settings.use_old_mdec_routines))
+      if (g_settings.use_old_mdec_routines) [[unlikely]]
       {
         const u16 a = ZeroExtend16(s_status.data_output_bit15.GetValue()) << 15;
         for (u32 i = 0; i < static_cast<u32>(s_block_rgb.size());)
@@ -769,7 +769,7 @@ bool MDEC::rl_decode_block(s16* blk, const u8* qt)
 void MDEC::IDCT(s16* blk)
 {
   // people have made texture packs using the old conversion routines.. best to just leave them be.
-  if (UNLIKELY(g_settings.use_old_mdec_routines))
+  if (g_settings.use_old_mdec_routines) [[unlikely]]
     IDCT_Old(blk);
   else
     IDCT_New(blk);
diff --git a/src/util/d3d11_texture.cpp b/src/util/d3d11_texture.cpp
index a5399da8a..f0c6548f9 100644
--- a/src/util/d3d11_texture.cpp
+++ b/src/util/d3d11_texture.cpp
@@ -114,7 +114,7 @@ void D3D11Framebuffer::SetDebugName(const std::string_view& name)
 
 void D3D11Framebuffer::CommitClear(ID3D11DeviceContext1* context)
 {
-  if (UNLIKELY(m_rt && m_rt->GetState() != GPUTexture::State::Dirty))
+  if (m_rt && m_rt->GetState() != GPUTexture::State::Dirty) [[unlikely]]
   {
     if (m_rt->GetState() == GPUTexture::State::Invalidated)
       context->DiscardView(m_rtv.Get());
@@ -124,7 +124,7 @@ void D3D11Framebuffer::CommitClear(ID3D11DeviceContext1* context)
     m_rt->SetState(GPUTexture::State::Dirty);
   }
 
-  if (UNLIKELY(m_ds && m_ds->GetState() != GPUTexture::State::Dirty))
+  if (m_ds && m_ds->GetState() != GPUTexture::State::Dirty) [[unlikely]]
   {
     if (m_ds->GetState() == GPUTexture::State::Invalidated)
       context->DiscardView(m_dsv.Get());
diff --git a/src/util/d3d12_device.cpp b/src/util/d3d12_device.cpp
index c3be9fc0e..24c3203ad 100644
--- a/src/util/d3d12_device.cpp
+++ b/src/util/d3d12_device.cpp
@@ -1563,7 +1563,7 @@ void D3D12Device::BeginRenderPass()
 
   ID3D12GraphicsCommandList4* cmdlist = GetCommandList();
 
-  if (LIKELY(m_current_framebuffer))
+  if (m_current_framebuffer) [[likely]]
   {
     D3D12Texture* rt = static_cast<D3D12Texture*>(m_current_framebuffer->GetRT());
     if (rt)
diff --git a/src/util/host.cpp b/src/util/host.cpp
index 660c2217a..617723704 100644
--- a/src/util/host.cpp
+++ b/src/util/host.cpp
@@ -36,7 +36,7 @@ std::pair<const char*, u32> Host::LookupTranslationString(const std::string_view
   s32 len;
 
   // Shouldn't happen, but just in case someone tries to translate an empty string.
-  if (UNLIKELY(msg.empty()))
+  if (msg.empty()) [[unlikely]]
   {
     ret.first = &s_translation_string_cache[0];
     ret.second = 0;
@@ -46,11 +46,11 @@ std::pair<const char*, u32> Host::LookupTranslationString(const std::string_view
   s_translation_string_mutex.lock_shared();
   ctx_it = s_translation_string_map.find(context);
 
-  if (UNLIKELY(ctx_it == s_translation_string_map.end()))
+  if (ctx_it == s_translation_string_map.end()) [[unlikely]]
     goto add_string;
 
   msg_it = ctx_it->second.find(msg);
-  if (UNLIKELY(msg_it == ctx_it->second.end()))
+  if (msg_it == ctx_it->second.end()) [[unlikely]]
     goto add_string;
 
   ret.first = &s_translation_string_cache[msg_it->second.first];
@@ -62,7 +62,7 @@ add_string:
   s_translation_string_mutex.unlock_shared();
   s_translation_string_mutex.lock();
 
-  if (UNLIKELY(s_translation_string_cache.empty()))
+  if (s_translation_string_cache.empty()) [[unlikely]]
   {
     // First element is always an empty string.
     s_translation_string_cache.resize(TRANSLATION_STRING_CACHE_SIZE);
diff --git a/src/util/vulkan_device.cpp b/src/util/vulkan_device.cpp
index 9f7a4edfe..93db7d479 100644
--- a/src/util/vulkan_device.cpp
+++ b/src/util/vulkan_device.cpp
@@ -2686,7 +2686,7 @@ void VulkanDevice::BeginRenderPass()
     VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, nullptr, VK_NULL_HANDLE, VK_NULL_HANDLE, {}, 0u, nullptr};
   std::array<VkClearValue, 2> clear_values;
 
-  if (LIKELY(m_current_framebuffer))
+  if (m_current_framebuffer) [[likely]]
   {
     VkFormat rt_format = VK_FORMAT_UNDEFINED;
     VkFormat ds_format = VK_FORMAT_UNDEFINED;