tensorrt_llm/common
file(GLOB SRCS *.cpp)
file(GLOB CU_SRCS *.cu)
add_library(common_src OBJECT ${SRCS} ${CU_SRCS})
set_property(TARGET common_src PROPERTY POSITION_INDEPENDENT_CODE ON)
set_property(TARGET common_src PROPERTY CUDA_RESOLVE_DEVICE_SYMBOLS ON)assert.h
namespace tensorrt_llm::common
{
[[noreturn]]inline void throwRuntimeError(const char* const file, int const line, std::string const& info = "")
{
throw TllmException(file, line, fmtstr("[TensorRT-LLM][ERROR] Assertion failed: %s", info.c_str()));
}
} // namespace tensorrt_llm::commonC++ 中的 [[noreturn]] 属性用于明确告知编译器和代码的阅读者:被标记的函数在任何情况下都不会返回到它的调用点。这意味着一旦程序执行进入这个函数,就不会再回到调用它的地方继续执行后续代码。
为了让你快速把握其核心,下表对比了 [[noreturn]] 函数与普通 void 函数的本质区别:
void 函数 | [[noreturn]] | |
|---|---|---|
| 控制流 | 不返回 | |
| 典型行为 | ||
| 编译器视角 |
主要用途与优势
使用 [[noreturn]] 属性主要能带来以下好处:
1. 编译器优化:这是最直接的作用。编译器得知函数不会返回后,可以执行特定优化。例如,它会识别出在该函数调用之后的所有代码都是死代码,并可能在编译时将其移除或发出警告。同时,对于不会返回的函数,编译器可能会省略一些为函数返回所做的准备(例如不必要的栈帧清理代码),从而优化生成的目标代码。 2. 提升代码质量与可读性:这个属性作为一种机器可读的文档,清晰地表明了函数的设计意图——它将中断正常的控制流。这极大地提高了代码的可读性和可维护性,让其他开发者或未来的你一眼就能明白该函数的行为影响。 3. 增强错误检查:编译器可以利用此属性提供更智能的警告。例如,如果一个被标记为 [[noreturn]]的函数内部竟然存在可以执行到函数末尾并隐式返回的路径,或者在其后书写了明显不会执行的代码,编译器可能会发出警告,帮助你发现潜在的逻辑错误。
典型适用场景
你通常会在以下类型的函数上使用 [[noreturn]] 属性:
• 终止程序的函数:例如,封装了 std::exit()、std::abort()的错误处理或致命退出函数。• 抛出异常的函数:那些其唯一目的就是抛出特定异常的函数。 • 无限循环:例如,某些任务调度器或服务器的主循环,它们设计上就是永不退出的。
重要注意事项与陷阱
使用 [[noreturn]] 时必须谨慎,否则会导致未定义行为:
• 核心规则:函数绝不能返回:这是最重要的原则。如果你给一个函数标记了 [[noreturn]],就必须确保它在所有执行路径上都绝对不会返回到调用者。如果某个路径下函数执行完毕并返回了,程序将进入未定义行为 的状态。在Debug模式下程序可能看似正常,但在Release模式下开启编译器优化后,由于编译器可能已经移除了返回后的处理逻辑,极易导致程序崩溃或产生不可预测的结果。• 不要滥用:这个属性并非用于提升性能的主要手段,其优化效果通常是局部的。只有在函数行为确实符合“不返回”的语义时才应使用。 • 与 void的区别:再次强调,void表示函数不返回值,但执行后会返回调用处。[[noreturn]]表示函数不返回控制流,执行后流程中断。
代码示例
#include <iostream>
#include <cstdlib> // 用于 std::exit
#include <stdexcept> // 用于 std::runtime_error
// 场景1: 终止程序
[[noreturn]]void fatalError(const std::string& message){
std::cerr << "Fatal Error: " << message << std::endl;
std::exit(EXIT_FAILURE); // 程序终止,不会返回
}
// 场景2: 抛出异常
[[noreturn]]void throwRuntimeError(const std::string& msg){
throw std::runtime_error(msg); // 抛出异常,控制流不会返回
}
// 场景3: 无限循环 (例如某些嵌入式系统的主循环)
[[noreturn]]void mainLoop(){
while (true) {
// ... 执行周期性任务
}
}
// 错误示例:此函数有条件返回,标记[[noreturn]]会导致未定义行为!
[[noreturn]]void problematicFunction(bool shouldExit){
if (shouldExit) {
std::exit(0);
}
// 如果 shouldExit 为 false,函数会执行到这里并返回,违反 [[noreturn]] 约定!
} // 危险!
int main(){
// fatalError("Disk full"); // 调用后程序终止,后面的代码不会执行
// std::cout << "This will never be printed.\n";
throwRuntimeError("Something went wrong");
// 抛出异常后,控制流也不会回到这里
return 0;
}总结
总而言之,[[noreturn]] 是一个有用的属性,但带有“破坏性”。它通过告知编译器控制流信息来辅助优化和错误检测,并作为代码文档。使用时务必严格遵守其核心约定:被标记的函数必须真正永不返回,否则会引发未定义行为。
#if defined(_WIN32)
#define TLLM_LIKELY(x) (__assume((x) == 1), (x))
#else
#define TLLM_LIKELY(x) __builtin_expect((x), 1)
#endif__builtin_expect((x), 1) 是 GCC 编译器提供的一个内置函数(其他如 Clang 也支持),其主要作用是为编译器提供分支预测信息,提示编译器表达式 x 的结果非常可能为真(即值等于1),从而帮助编译器优化代码,提升程序运行效率。
为了让您快速掌握其核心要点,下表总结了关键信息:
| 函数原型 | long __builtin_expect (long exp, long c) |
| 本语句含义 | x 的值很可能为真(即等于1) |
| 返回值 | exp(即 x)的值本身 |
| 核心目的 | |
| 常见封装 | likely(x) 宏 |
工作原理与优化效应
现代CPU普遍采用指令流水线和预取机制来提高效率。当处理器遇到条件分支(如 if 语句)时,它需要“猜测”接下来会执行哪个分支(称为分支预测),并提前将相应指令加载到高速缓存中。如果预测错误,已经预取和部分执行的指令就需要被丢弃,然后重新加载正确分支的指令,这个过程会导致流水线清空,造成性能损失。
__builtin_expect((x), 1) 的作用就是向编译器传递一个明确的提示:“x 为真的可能性极大”。编译器在生成汇编代码时,会将可能性更高的代码块(if 块)紧挨着条件判断指令之后排列,而将可能性较小的代码块(else 块)放在相对较远的位置,通常需要通过跳转指令才能到达。
这样做的好处是:
• 提高缓存局部性:最常执行的指令在内存中连续分布,更有可能已被预取到高速缓存中,减少了CPU等待数据的时间。 • 减少分支预测错误惩罚:由于代码布局符合预期执行流程,CPU的分支预测器做出正确预测的概率更高,从而显著减少了因预测错误导致的流水线清空和刷新。
实际应用方式
在实际编程中,__builtin_expect 通常不会被直接使用,而是被定义成宏,以提高代码的可读性。最常见的宏是 likely 和 unlikely。
#define likely(x) __builtin_expect(!!(x), 1) // 表示x很可能为真
#define unlikely(x) __builtin_expect(!!(x), 0) // 表示x很可能为假这里的 !!(x) 是一个小技巧,它的作用是将参数 x 强制转换为逻辑上的布尔值(0或1)。无论 x 是什么类型或值,!!(x) 的结果都严格等于0(假)或1(真)。
一个典型的使用场景是在错误检查或异常处理中,因为这些情况通常很少发生。
// 检查指针ptr是否有效,正常情况下大概率是有效的
if (likely(ptr != nullptr)) {
// 编译器会优化代码,优先保证这个主流程的指令顺序紧凑、连续
do_something_with(ptr);
} else {
// 错误处理路径:概率很小,编译器可能会将其放在远离主流程的位置
handle_error();
}
// 检查一个非常罕见的错误条件
if (unlikely(some_rare_error_condition)) {
// 这个小概率分支的代码会被特殊安排
log_error();
return ERROR_CODE;
}重要注意事项
1. 不要滥用:只有在某个分支的执行概率具有非常明显的偏向性(例如,99% vs 1%)时,使用 __builtin_expect才能带来可观的性能提升。如果分支概率接近50/50,使用它可能收效甚微,甚至可能因为错误的提示而略微降低性能。GCC官方文档也建议,相比于程序员的主观预测,使用-fprofile-arcs选项通过实际运行来分析分支概率是更科学的方法。2. 不影响逻辑:需要明确的是, if (likely(x))在语法上完全等价于if (x)。这个宏只影响编译优化,不会改变程序的逻辑。它的返回值就是第一个参数x本身。3. 编译器兼容性:这是GCC的扩展功能。在编写可移植代码时,通常需要检查编译器类型,对于不支持该特性的编译器,需要提供回退方案。 #if defined(__GNUC__) || defined(__clang__)
#define LIKELY(x) __builtin_expect(!!(x), 1)
#define UNLIKELY(x) __builtin_expect(!!(x), 0)
#else
#define LIKELY(x) (x) // 对于其他编译器,直接定义为原表达式
#define UNLIKELY(x) (x)
#endif
总结
总而言之,__builtin_expect((x), 1) 是一个用于性能微调的低级工具。它通过向编译器提供明确的分支概率提示,帮助优化代码布局,从而在现代处理器上获得更好的执行效率。在实际应用中,建议通过 likely/unlikely 宏来使用它,并严格用于那些执行概率有明显偏差的关键路径上。
#define TLLM_CHECK(val) \
do \
{ \
TLLM_LIKELY(static_cast<bool>(val)) ? ((void) 0) \
: tensorrt_llm::common::throwRuntimeError(__FILE__, __LINE__, #val); \
} while (0)
#define TLLM_CHECK_WITH_INFO(val, info) \
do \
{ \
TLLM_LIKELY(static_cast<bool>(val)) ? ((void) 0) \
: tensorrt_llm::common::throwRuntimeError(__FILE__, __LINE__, info); \
} while (0)
#define TLLM_THROW(...) \
do \
{ \
throw NEW_TLLM_EXCEPTION(__VA_ARGS__); \
} while (0)
#define TLLM_WRAP(ex) \
NEW_TLLM_EXCEPTION("%s: %s", tensorrt_llm::common::TllmException::demangle(typeid(ex).name()).c_str(), ex.what())在C和C++编程中,((void)0) 是一个经典的技巧,主要用于在需要表达式的地方提供一个明确不执行任何操作(no-op)且不产生任何值的占位符。为了让你快速抓住要点,我先用一个表格来总结它的核心用途和原理。
| 核心作用 | |
| 关键原理 | 0 强制转换为 void 类型,表示该表达式的值“不存在”且不应被使用 |
| 主要优势1 | 消除编译器警告 |
| 主要优势2 | 防止误用 |
| 经典场景 | assert宏或调试宏) |
详细说明与应用场景
工作原理
这个技巧的核心在于类型转换。字面值 0 本身是一个 int 类型的表达式。当你使用 (void)0 时,你执行了一次显式的类型转换,将 0 这个值强制转换为 void 类型。在C/C++标准中,void 类型表示“无类型”,一个 void 类型的表达式不表示任何实际的值,因此它不能被用于需要值的上下文中(比如赋值给变量或参与运算)。同时,计算这个表达式本身不产生任何实际的可执行代码(即无操作)。
常见应用场景
1. 宏定义中的安全占位符
这是((void)0)最经典和常见的用法。在编写宏时,尤其是在条件编译中,经常需要定义一个在特定情况下(如发布模式)什么都不做的宏。直接使用一个空的#define可能在语法上不完整,而使用((void)0)则能提供一个符合语法、功能正确且安全的空语句。#ifdef DEBUG
#define DEBUG_PRINT(x) printf(x)
#else
#define DEBUG_PRINT(x) ((void)0) // 在非调试模式下,该宏展开后什么都不做
#endif在上面的例子中,当
DEBUG未定义时,代码中所有DEBUG_PRINT("message")都会被替换为((void)0)。这样做的好处是,即使你在if语句等后面不加分号直接调用这个宏,比如DEBUG_PRINT("test"),展开后是((void)0);,这依然是一个合法的空语句,不会导致语法错误。2. 断言宏的实现
在许多库的断言宏实现中,也能看到(void)0的身影。它通常被用在断言成功的分支上。#define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))在这里,如果表达式
expr为真,则断言成功,执行(void)0(无操作);如果为假,则调用错误处理函数assert_failed。
注意事项与替代方案
• 与 void*的区别:务必分清(void)0和((void*)0)。前者产生一个void类型的值表达式,用于表示“无值”。后者产生一个void*类型的空指针常量,通常用来定义NULL,表示指针不指向任何对象。• C++中的 static_cast:在C++中,为了更符合现代类型转换风格,可以使用static_cast<void>(0)来达到完全相同的效果,它们在功能上是等价的。• 编译器的特殊支持:某些编译器提供了类似功能的内在函数,例如微软VC++的 __noop。但((void)0)因其标准性和可移植性而被广泛采用。
总而言之,((void)0) 是一个巧妙运用C/C++类型系统来实现安全、无副作用的空表达式的最佳实践。它在宏定义、条件编译等场景中尤为重要,是编写健壮和可移植代码的一个有用工具。
C++ 中的 typeid 操作符是运行时类型识别(RTTI)机制的核心组成部分,它允许程序在运行时查询变量、对象或类型的详细信息。下面这个表格汇总了它的核心特性,帮助你快速把握要点。
| 主要功能 | |
| 返回值类型 | const std::type_info 对象的引用 |
| 关键方法 | .name()==, !=) |
| 多态类型 | |
| 非多态类型 | |
| 使用头文件 | #include <typeinfo> |
基本用法与注意事项
使用 typeid 需要包含 <typeinfo> 头文件。它的返回值是一个 const std::type_info&,你可以利用这个对象进行类型比较或获取类型名称。
#include <iostream>
#include <typeinfo>
int main(){
int i = 0;
const std::type_info& ti = typeid(i);
std::cout << "Type of i: " << ti.name() << std::endl; // 输出类型名称,如 'i' 表示 int
// 直接比较类型
if (typeid(i) == typeid(int)) {
std::cout << "i is definitely an int." << std::endl;
}
return 0;
}需要注意,std::type_info::name() 返回的类型名称字符串是编译器相关的,可能可读性不强(例如,GCC 中 int 可能显示为 "i")。如果需要更友好的显示,可以考虑使用像 abi::__cxa_demangle (GCC/Clang) 这样的解译函数。
理解静态类型与动态类型
typeid 最核心的行为取决于操作数是否为多态类型(即至少包含一个虚函数的类)。
• 非多态类型(静态类型):对于没有虚函数的类型(包括基本数据类型,如 int、double),或者指向非多态类型的指针本身,typeid在编译时即可确定结果,返回的是表达式或变量的静态类型(声明类型)。class BaseWithoutVirtual {};
class Derived : public BaseWithoutVirtual {};
Derived d;
BaseWithoutVirtual* bp = &d;
std::cout << typeid(*bp).name() << std::endl;
// 输出 BaseWithoutVirtual 的类型名,因为基类没有虚函数,识别的是静态类型• 多态类型(动态类型):当对多态类型的对象、引用或指针解引用(如 *ptr)使用时,typeid会在运行时确定其动态类型(实际指向的对象的类型)。class Base {
public:
virtual ~Base() {} // 至少有一个虚函数,使Base成为多态类型
};
class Derived : public Base {};
Derived d;
Base* bp = &d; // 基类指针指向派生类对象
std::cout << typeid(*bp).name() << std::endl;
// 输出 Derived 的类型名,因为基类有虚函数,识别的是动态类型
重要陷阱与安全用法
1. 空指针解引用:对空指针进行解引用操作以获取类型信息会导致 std::bad_typeid异常。Base* bp = nullptr;
try {
// 错误!对空指针bp解引用
std::cout << typeid(*bp).name() << std::endl;
} catch (const std::bad_typeid& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
}安全的做法是首先检查指针是否有效,或者直接对指针本身使用
typeid(但这会返回指针类型,而不是所指对象的类型)。2. 类型比较的局限性: typeid比较的是类型的精确匹配。基类和派生类被认为是不同的类型。typeid会忽略顶层的const和volatile限定符,即typeid(const int) == typeid(int)结果为true。
实践建议与替代方案
• 谨慎使用:RTTI 会带来一定的运行时开销。如果可以通过虚函数等设计在编译时确定行为,通常是更好的选择。 • 类型名称的可移植性:不要依赖 name()返回字符串的格式,因为它因编译器而异。• 考虑替代方案:在模板编程或需要编译期类型判断时,可以考虑使用 <type_traits>等现代 C++ 特性,它们通常更高效。
总结
typeid 操作符是 C++ RTTI 的关键工具,主要用于在运行时确定类型信息,特别是在涉及多态类型的场景下。使用时需牢记其静态类型与动态类型的区别,并注意空指针安全问题。虽然在某些调试或特定框架中很有用,但在追求高性能的代码中应权衡其开销。
Memory Allocator
enum class ReallocType
{
INCREASE,
REUSE,
DECREASE,
};class IAllocator
{
public:
virtual ~IAllocator() = default;
// no copying
IAllocator(const IAllocator&) = delete;
IAllocator& operator=(const IAllocator&) = delete;
template <typename T>
[[nodiscard]] T* reMalloc(T* ptr, size_t sizeBytes, const bool setZero = true)
{
TLLM_LOG_DEBUG(__PRETTY_FUNCTION__);
// TODO martinma: why do we need this size extension?
auto const sizeAligned = ((sizeBytes + 31) / 32) * 32; // make the buffer align with 32 bytes
if (contains(ptr))
{
auto const realloc = reallocType(ptr, sizeAligned);
if (realloc == ReallocType::INCREASE)
{
TLLM_LOG_DEBUG("ReMalloc the buffer %p since it is too small.", ptr);
free(&ptr);
return reinterpret_cast<T*>(malloc(sizeAligned, setZero));
}
else if (realloc == ReallocType::DECREASE)
{
TLLM_LOG_DEBUG("ReMalloc the buffer %p to release unused memory to memory pools.", ptr);
free(&ptr);
return reinterpret_cast<T*>(malloc(sizeAligned, setZero));
}
else
{
assert(realloc == ReallocType::REUSE);
TLLM_LOG_DEBUG("Reuse original buffer %p with size %d and do nothing for reMalloc.", ptr, sizeAligned);
if (setZero)
{
memSet(ptr, 0, sizeAligned);
}
return ptr;
}
}
else
{
TLLM_LOG_DEBUG("Cannot find buffer %p, mallocing new one.", ptr);
return reinterpret_cast<T*>(malloc(sizeAligned, setZero));
}
}
virtual void free(void** ptr)= 0;
template <typename T>
void free(T** ptr)
{
free(reinterpret_cast<void**>(ptr));
}
protected:
IAllocator() = default;
virtual void* malloc(std::size_t size, bool setZero)= 0;
virtual void memSet(void* ptr, int val, std::size_t size)= 0;
virtual bool contains(void const* ptr) const= 0;
virtual ReallocType reallocType(void const* ptr, size_t size) const= 0;
};在 C 中,`[[nodiscard]]` 是一个属性标识符,用于告诉编译器和代码的阅读者:某个函数的返回值**非常重要,不应该被忽略**。自 C17 引入以来,它成为提升代码安全性和健壮性的重要工具。
核心含义与基本用法
[[nodiscard]] 的核心含义是“不应舍弃”。当调用被它标记的函数时,如果其返回值没有被有意识地处理(例如,没有赋值给变量,也没有用于条件判断),编译器会发出警告。
它的基本语法如下:
• 在 C++17 中: [[nodiscard]]• 在 C++20 中进行了扩展,允许附带说明信息: [[nodiscard("提示字符串")]]
下面是一个简单的例子:
// 标记一个函数的返回值为 nodiscard
[[nodiscard]]int importantFunction(){
return 42;
}
int main(){
importantFunction(); // ⚠️ 编译器警告:返回值被忽略!
int value = importantFunction(); // ✅ 正确:返回值被保存
static_cast<void>(importantFunction()); // ✅ 正确:显式丢弃,通常不会警告
return 0;
}在 C++20 中,你可以提供更清晰的提示:
[[nodiscard("请检查操作是否成功")]]bool performOperation(){
return true;
}应用范围与场景
[[nodiscard]] 不仅可以标记函数,还可以标记类、枚举等,应用场景广泛。
| 函数返回值 | ||
| 类/结构体 | [[nodiscard]],则该类的任何构造函数返回的临时对象都不应被忽略。 | |
| 枚举类型 | ||
| 构造函数 |
为什么要使用 [[nodiscard]]?
使用 [[nodiscard]] 主要目的是在编译期捕获因疏忽导致的潜在错误,从而:
• 防止资源泄漏:例如,忽略一个返回 std::unique_ptr的工厂函数的结果,会导致分配的资源立刻被释放。• 强制错误检查:确保函数返回的错误码或成功状态得到检查,避免程序在错误状态下继续运行。 • 保证计算结果被使用:避免无意义的函数调用,提高代码质量。
需要注意的细节与陷阱
1. 对返回引用或指针无效:如果函数返回的是被标记为 [[nodiscard]]的类的引用或指针,编译器通常不会发出警告。因为这种返回形式可能表示返回的是已存在的对象,而非需要接收的新资源。2. 可以有意绕过:虽然 [[nodiscard]]会发出警告,但开发者仍然可以通过将返回值赋值给变量或使用std::ignore来明确表示“我已知晓,并有意忽略”。这体现了 C++“相信程序员”的理念。3. 谨慎用于作用域守卫(Scope Guard):如果一个函数返回一个作用域守卫对象(依靠其析构函数进行清理),标记 [[nodiscard]]通常是正确且必要的。这可以防止守卫对象被立即销毁。但需注意,用户仍可能通过std::ignore等方式忽略它,导致清理操作未按预期执行。
编译器支持
主流编译器如 GCC (7+)、Clang (3.9+) 和 MSVC (2017 15.3+) 都提供了对 [[nodiscard]] 的支持。
[[nodiscard]] 是一个低开销、高效的工具,它能帮助你在编译阶段就发现许多因粗心导致的错误。在编写新的 API,尤其是涉及资源管理、错误状态返回的函数时,积极地使用它是一个非常好的习惯。
希望这些解释能帮助你更好地理解 [[nodiscard]]。如果你有特定的使用场景想讨论,我很乐意听听你的想法。
在C++中,__PRETTY_FUNCTION__ 是一个编译器扩展宏,主要用于调试和日志记录,它能提供当前函数的详细签名信息。
下面的表格快速对比了C++中几个常见的函数名宏/标识符:
__PRETTY_FUNCTION__ | void MyClass::myFunction(int, double) const | ||
__FUNCSIG__ | void __cdecl MyClass::myFunction(int, double) const | __PRETTY_FUNCTION__,但用于Visual Studio编译器。 | |
__func__ | myFunction | ||
__FUNCTION__ | myFunction | __func__相同,为保证兼容性而存在。 |
💡 核心用途与示例
__PRETTY_FUNCTION__ 的核心价值在于,当你在一个函数内部使用它时,它会展开为一个包含函数完整签名的字符串。这对于以下场景特别有用:
• 调试程序:快速确认代码执行流进入了哪个特定的函数重载或模板实例。 • 日志记录:在日志中输出详细的函数调用信息,便于追踪问题。
例如,在下面的代码中:
#include <iostream>
class MyClass {
public:
void myMethod(int x){
std::cout << "__PRETTY_FUNCTION__: " << __PRETTY_FUNCTION__ << std::endl;
}
};
int main(){
MyClass obj;
obj.myMethod(10);
return 0;
}运行后可能会输出:__PRETTY_FUNCTION__: void MyClass::myMethod(int)。这清晰地显示了函数所属的类、函数名和参数类型。
对于C++模板和重载函数,__PRETTY_FUNCTION__ 的优势更加明显,它能明确显示出模板参数的具体类型和函数签名,帮助区分不同的实例化版本或重载。
⚠️ 重要注意事项
1. 编译器依赖性: __PRETTY_FUNCTION__并非C标准的一部分,是GCC和Clang等编译器的扩展。如果你需要编写跨编译器的代码,需要注意这一点。在Microsoft Visual C (MSVC) 中,等效的宏是__FUNCSIG__。2. C++20 的现代替代方案:从C++20开始,标准库引入了 std::source_location。它提供了一个更标准、更可移植的方式来获取代码信息,包括函数名、文件名、行号等。#include <iostream>
#include <source_location>
void log(const std::source_location& loc = std::source_location::current()){
std::cout << "Function: " << loc.function_name() << std::endl;
std::cout << "File: " << loc.file_name() << " at line " << loc.line() << std::endl;
}
int main(){
log(); // 会自动捕获main函数中调用位置的信息
}在新项目中,如果目标是C++20或更高,并且需要跨平台兼容性,推荐优先考虑使用
std::source_location。
总结
总而言之,__PRETTY_FUNCTION__ 是一个在GCC/Clang环境下非常实用的调试工具,它能提供丰富的函数签名信息。但在使用时,请留意其编译器特异性,并了解C++20提供了更现代化的替代方案 std::source_location。
代码 auto const sizeAligned = ((sizeBytes + 31) / 32) * 32; 是一种常见的内存对齐编程技巧。它的核心目的是将原始数据大小 (sizeBytes) 向上舍入到最接近的 32 字节的整数倍,其带来的好处主要体现在性能和兼容性上。
下面的表格详细说明了这种对齐操作带来的主要好处、背后的原因以及典型应用场景。
| 提升CPU访问效率 | ||
| 满足指令集要求 | ||
| 优化缓存利用率 | ||
| 避免平台差异问题 |
代码是如何实现对齐的?
这行代码 ((sizeBytes + 31) / 32) * 32 是一个经典的“向上取整”算法:
• sizeBytes + 31:首先,将原始大小加上一个比对齐边界(32)小1的值(31)。这相当于预先分配出可能需要的最大填充空间。• / 32:然后,进行整数除法。整数除法会自动截断小数部分,结果相当于“向下取整”到32的倍数。• * 32:最后,再乘以32,将“向下取整”的结果恢复为32的倍数,这样就实现了“向上取整”。
例如,如果 sizeBytes 是 50:
• (50 + 31) = 81• 81 / 32 = 2(整数除法)• 2 * 32 = 64
最终,对齐后的大小sizeAligned就是 64。
权衡与注意事项
虽然内存对齐好处很多,但它是一种典型的 “以空间换时间” 的策略。你可能会在成员之间和结构体末尾引入一些为对齐而存在的填充字节,从而增加总的内存占用。
在绝大多数对性能要求高的场景下(如游戏引擎、高频交易、科学计算),这种微小的空间开销换来的性能提升是非常值得的。但在内存极其受限的嵌入式环境中,可能需要谨慎权衡,甚至使用 #pragma pack 等指令减少填充。
总结
总而言之,将缓冲区对齐到32字节,主要是为了压榨CPU性能和确保程序正确性。这是一个在系统编程和高性能计算中常用的重要优化手段。
参考文献
• https://github.com/NVIDIA/TensorRT-LLM/tree/release/0.5.0/cpp/tensorrt_llm/common

夜雨聆风