面试真题 | 中科曙光C++[20241008]
自我介绍;
针对项目:
CNN模型、损失函数、评价指标、改进方向、计算加速;
CNN模型
CNN,即卷积神经网络,是一种专门用于处理具有类似网格结构数据的深度学习模型。它通过卷积层和池化层提取图像特征,并通过全连接层进行分类或回归预测。CNN在图像识别、目标检测和图像生成等领域取得了巨大成功。
具体来说,CNN的模型结构包括输入层、卷积层、激活函数、池化层、全连接层和输出层。输入层接收图像数据,并将其转换为CNN可以理解的数字形式。卷积层通过滤波器(卷积核)对图像进行卷积运算,提取图像特征。激活函数如ReLU、Sigmoid等,用于引入非线性,增强模型的表达能力。池化层通过最大池化或平均池化等操作,减少特征图的维度,降低计算量,同时保留关键特征。全连接层将特征图展平为一维向量,并通过加权求和等方式进行分类或回归预测。输出层给出最终的预测结果。
面试官追问:
- CNN在处理图像数据时有哪些优势?
- 你能解释一下卷积层和池化层在CNN中的作用吗?
- 在嵌入式设备上部署CNN模型时,需要考虑哪些因素?
损失函数
损失函数是用来评估模型输出结果与真实标签之间误差大小的函数。在训练过程中,模型通过损失函数计算损失值,并利用优化算法来最小化损失值,从而不断优化模型参数,提高模型性能。
常见的损失函数包括均方误差(MSE)和交叉熵损失函数等。均方误差主要用于回归任务,衡量模型预测值与真实值之间的平方差。交叉熵损失函数则常用于分类问题,衡量模型预测的概率分布与真实标签的概率分布之间的差异。
面试官追问:
- 除了MSE和交叉熵损失函数外,你还知道哪些其他的损失函数?
- 在选择损失函数时,需要考虑哪些因素?
- 能否解释一下损失函数是如何在模型训练过程中起作用的?
评价指标
评价指标是用于考核、评估、比较模型性能的统计指标。在机器学习和深度学习中,常用的评价指标包括准确率、召回率、F1分数、AUC等。
准确率衡量的是模型正确预测的样本数占总样本数的比例。召回率则衡量的是模型正确预测出的正例样本数占实际正例样本数的比例。F1分数是准确率和召回率的调和平均,用于综合评估模型的性能。AUC则常用于二分类问题中,衡量模型在不同阈值下的性能表现。
面试官追问:
- 在不同的应用场景下,如何选择合适的评价指标?
- 你能解释一下AUC的含义和计算方法吗?
- 除了准确率、召回率和F1分数外,还有哪些常用的评价指标?
改进方向
针对CNN模型的改进方向有很多,包括模型结构优化、算法改进、数据集增强等。
在模型结构优化方面,可以尝试使用更深的网络结构、更复杂的卷积核设计、引入注意力机制等方法来提高模型的表达能力。在算法改进方面,可以尝试使用更高效的优化算法、正则化方法等来减少模型的过拟合和提高泛化能力。在数据集增强方面,可以通过数据扩增、数据清洗等方法来提高数据的质量和多样性。
面试官追问:
- 你能给出一些具体的模型结构优化方法吗?
- 在算法改进方面,有哪些常用的正则化方法?
- 数据集增强对模型性能的提升有何影响?
计算加速
在嵌入式设备上部署CNN模型时,计算加速是一个非常重要的问题。常见的计算加速方法包括硬件加速和软件优化两个方面。
硬件加速方面,可以使用GPU、FPGA等专用硬件来加速模型的计算过程。这些硬件具有强大的并行计算能力,可以显著提高模型的计算速度。软件优化方面,可以通过优化算法、减少计算量、使用高效的计算库等方法来提高模型的计算效率。
面试官追问:
1. 在嵌入式设备上使用GPU进行加速时,需要注意哪些问题?
在嵌入式设备上使用GPU进行加速时,需要注意以下几个关键问题:
- GPU选择:确保所选择的GPU与嵌入式设备的兼容性,并考虑其功耗、散热以及计算能力是否满足需求。NVIDIA的GPU在深度学习领域应用广泛,但具体选择还需根据设备和应用场景进行权衡。
- 散热与功耗:嵌入式设备通常对功耗和散热有严格要求。因此,在使用GPU进行加速时,需要密切关注GPU的功耗和散热情况,以避免因过热或功耗过高而导致的设备故障。
- 数据传输:GPU加速的关键在于高效的数据传输。如果数据传输的速度跟不上计算的速度,GPU可能会空闲下来,导致计算能力的浪费。因此,需要优化数据传输路径,减少数据传输延迟,提高整体计算效率。
- 编程与兼容性:使用GPU进行深度学习需要一定的编程技巧,如CUDA编程。同时,需要确保所使用的深度学习框架(如TensorFlow、PyTorch等)支持GPU加速,并正确配置GPU资源。此外,还需注意不同框架之间的兼容性问题。
- 内存管理:GPU的内存管理也是一大挑战。需要合理分配和管理GPU内存资源,避免内存泄漏和碎片化问题。同时,还需考虑如何优化内存访问模式,以提高内存利用率和计算效率。
2. 你能给出一些具体的软件优化方法吗?
在嵌入式设备上部署CNN模型时,可以采用以下具体的软件优化方法:
- 算法优化:选择适合嵌入式设备的算法和数据结构,减少计算复杂度和内存占用。例如,可以采用高效的数值计算算法和信号处理技术,提高系统响应速度和精度。
- 模型剪枝与量化:对CNN模型进行剪枝和量化操作,可以减少模型的参数数量和计算量,从而提高计算效率。剪枝操作可以移除对模型性能影响较小的权重和神经元,而量化操作则可以将模型的权重和激活值转换为较低精度的表示形式。
- 代码优化:优化代码结构,减少不必要的计算和数据传输。例如,可以通过合并相似的计算操作、减少循环嵌套等方式来降低计算复杂度。同时,还需注意代码的可读性和可维护性。
- 使用高效的计算库:利用现有的高效计算库(如cuDNN、TensorRT等)来加速模型的计算过程。这些库提供了针对GPU优化的高性能计算函数,可以显著提高计算效率。
3. 能否解释一下硬件加速和软件优化在模型计算加速中的作用?
硬件加速和软件优化在模型计算加速中起着至关重要的作用。具体来说:
- 硬件加速:硬件加速主要通过使用高性能的硬件资源(如GPU、FPGA等)来加速模型的计算过程。这些硬件资源具有强大的并行计算能力,可以显著提高模型的计算速度。同时,硬件加速还可以减少数据传输延迟和内存访问冲突等问题,从而提高整体计算效率。
- 软件优化:软件优化则主要通过改进算法、优化代码结构和使用高效的计算库等方式来提高模型的计算效率。软件优化可以减少不必要的计算和数据传输,降低计算复杂度和内存占用。同时,通过优化内存访问模式和内存管理策略等方式,还可以提高内存利用率和计算效率。
指针和引用区别;
指针和引用是C++中两种用于间接访问对象或函数的方式,但它们在使用和特性上有显著的区别。
指针
-
定义与初始化:
- 指针是一个变量,其存储的是另一个变量的内存地址。
- 指针需要显式地使用
*
(解引用操作符)来访问指向的变量。 - 指针可以初始化为
nullptr
(或NULL
,但在C++11及以后推荐使用nullptr
),表示它不指向任何有效的内存地址。
-
内存占用:
- 指针本身占用一定的内存空间,用于存储地址。
-
灵活性:
- 指针可以指向任何类型的变量,包括不同类型的对象、数组、函数等。
- 指针可以进行算术运算(如加减操作),以访问相邻的内存位置。
- 指针可以改变指向(即重新赋值)。
-
安全性:
- 指针操作容易出错,如野指针(未初始化或已释放的指针)、空指针解引用、悬挂指针(指向已释放内存的指针)等。
- 需要程序员手动管理指针的生命周期。
引用
-
定义与初始化:
- 引用是一个别名,它必须在定义时被初始化,且一旦初始化后就不能再改变指向。
- 引用使用
&
(引用操作符)来创建,但访问引用时不需要使用&
。
-
内存占用:
- 引用本身不占用额外的内存空间,它只是原始变量的另一个名字。
-
灵活性:
- 引用的类型必须与所引用的对象类型匹配(或在某些情况下兼容)。
- 引用不能为空,也不能改变指向。
- 引用不能进行算术运算。
-
安全性:
- 引用比指针更安全,因为它们避免了空指针和悬挂指针的问题。
- 引用的生命周期由它所引用的对象的生命周期决定,因此不需要手动管理。
可能的追问及回答
追问1:在函数参数传递中,指针和引用有什么区别?
回答:
- 使用指针作为函数参数时,可以传递变量的地址,允许函数内部修改原始变量的值(通过解引用指针)。此外,指针还可以传递空值(
nullptr
),表示不指向任何对象。 - 使用引用作为函数参数时,同样可以修改原始变量的值(因为引用是原始变量的别名),但引用不能为空。这有助于避免空指针解引用的错误。另外,引用在语法上更简洁,且避免了使用指针时的解引用操作。
追问2:在嵌入式系统中,使用指针和引用时需要注意哪些问题?
回答:
- 在嵌入式系统中,内存资源通常有限。因此,使用指针时需要特别小心内存泄漏、野指针和悬挂指针等问题。同时,要注意指针的算术运算可能导致访问非法内存区域。
- 使用引用时,虽然避免了指针的一些安全性问题,但仍然需要注意引用的生命周期。确保引用的对象在引用存在期间始终有效。此外,由于引用不能为空,因此在某些情况下可能需要额外的逻辑来处理可能的空值情况(例如,使用可选类型或特殊值来表示空)。
追问3:在C++中,有没有一种方式可以既像指针一样灵活,又像引用一样安全?
回答:
- C++11引入了智能指针(如
std::unique_ptr
和std::shared_ptr
),它们提供了一种既灵活又安全的内存管理方式。智能指针可以像指针一样进行动态内存分配和释放,同时自动管理内存的生命周期,避免了内存泄漏和悬挂指针的问题。此外,智能指针还可以像引用一样提供对原始对象的访问(通过解引用操作),并且支持空值检查(通过比较智能指针与nullptr
)。
通过以上回答和追问,可以展示候选人对指针和引用区别的深入理解,以及在实际编程中如何根据具体需求选择使用指针或引用的能力。同时,也展示了候选人在嵌入式系统编程中对内存管理和安全性问题的关注。
程序崩溃怎么调试;
std::shared_ptr;
回答
std::shared_ptr
是 C++11 标准库引入的一种智能指针,用于自动管理动态分配的内存,以避免手动管理内存时容易出现的内存泄漏和悬挂指针等问题。std::shared_ptr
通过引用计数机制来实现多个指针共享同一块内存的功能,当最后一个 std::shared_ptr
被销毁或重置时,它所管理的内存才会被释放。
主要特性
-
引用计数:
std::shared_ptr
内部维护一个引用计数,用于跟踪有多少个std::shared_ptr
实例共享同一块内存。当一个新的std::shared_ptr
被创建并指向已存在的内存时,引用计数会增加;当一个std::shared_ptr
被销毁或重置为指向其他内存时,引用计数会减少。当引用计数变为零时,内存会被释放。 -
线程安全:
std::shared_ptr
的引用计数操作是线程安全的,可以在多线程环境中安全地使用。 -
自定义删除器:
std::shared_ptr
允许用户指定一个自定义的删除器,用于在内存释放时执行特定的操作。 -
类型转换:
std::shared_ptr
支持通过std::dynamic_pointer_cast
、std::static_pointer_cast
和std::const_pointer_cast
进行类型转换。 -
弱引用:
std::weak_ptr
是与std::shared_ptr
配套使用的弱引用智能指针,它不会增加引用计数,因此不会阻止std::shared_ptr
所管理的内存被释放。std::weak_ptr
可以用于解决循环引用的问题。
使用示例
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() { std::cout << "MyClass Constructor" << std::endl; }
~MyClass() { std::cout << "MyClass Destructor" << std::endl; }
void doSomething() { std::cout << "Doing something..." << std::endl; }
};
int main() {
// 创建一个 std::shared_ptr 并指向一个 MyClass 对象
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
// 使用 std::shared_ptr 调用成员函数
ptr1->doSomething();
// 创建一个新的 std::shared_ptr 并指向相同的 MyClass 对象
std::shared_ptr<MyClass> ptr2 = ptr1;
// 此时 ptr1 和 ptr2 都指向同一个 MyClass 对象,引用计数为 2
// 当 ptr1 和 ptr2 都超出作用域时,MyClass 对象才会被销毁
// 使用自定义删除器(示例中未使用,但可以通过 std::shared_ptr 的构造函数指定)
// auto customDeleter = [](MyClass* p) { delete p; std::cout << "Custom deleter called" << std::endl; };
// std::shared_ptr<MyClass> ptr3(new MyClass(), customDeleter);
return 0;
}
面试官追问
-
追问1:
std::shared_ptr
的引用计数是如何实现的?- 回答:
std::shared_ptr
的引用计数通常是通过一个控制块(control block)来实现的,这个控制块包含了引用计数、指向实际对象的指针以及可能的其他信息(如自定义删除器)。当创建一个新的std::shared_ptr
实例时,它会检查是否已经存在一个控制块,如果存在,则增加引用计数;如果不存在,则创建一个新的控制块并初始化引用计数为 1。当std::shared_ptr
被销毁或重置时,它会减少引用计数,如果引用计数变为零,则释放实际对象并销毁控制块。
- 回答:
-
追问2:
std::shared_ptr
和std::unique_ptr
有什么区别?- 回答:
std::unique_ptr
是另一种智能指针,但它与std::shared_ptr
的主要区别在于所有权模型。std::unique_ptr
拥有它所管理的对象的唯一所有权,这意味着同一时间内只能有一个std::unique_ptr
指向同一个对象。相比之下,std::shared_ptr
允许多个指针共享同一个对象的所有权,通过引用计数机制来管理内存。
- 回答:
-
追问3:如何解决
std::shared_ptr
引起的循环引用问题?- 回答:循环引用是指两个或多个对象通过
std::shared_ptr
相互引用,导致它们的引用计数永远不会变为零,从而内存无法被释放。为了解决这个问题,可以使用std::weak_ptr
。std::weak_ptr
是一种不增加引用计数的智能指针,它可以指向由std::shared_ptr
管理的对象,但不会阻止std::shared_ptr
所管理的内存被释放。因此,可以通过在循环引用的一方使用std::weak_ptr
来打破循环引用。
- 回答:循环引用是指两个或多个对象通过
-
追问4:
std::shared_ptr
的性能如何?- 回答:
std::shared_ptr
的性能通常是可以接受的,但在某些情况下可能会成为瓶颈。例如,当创建和销毁大量std::shared_ptr
实例时,引用计数的增加和减少操作可能会导致性能下降。此外,由于std::shared_ptr
需要维护控制块和引用计数,因此它的内存占用可能比原始指针要大。然而,在大多数情况下,std::shared_ptr
提供的内存安全和方便性足以抵消其性能上的开销。
- 回答:
-
追问5:如何在多线程环境中安全地使用
std::shared_ptr
?- 回答:
std::shared_ptr
的引用计数操作是线程安全的,因此可以在多线程环境中安全地使用。但是,需要注意的是,虽然std::shared_ptr
本身可以安全地在多线程之间共享,但它所管理的对象并不是线程安全的。如果对std::shared_ptr
所管理的对象进行多线程访问,需要确保访问是同步的,以避免数据竞争和未定义行为。这通常可以通过使用互斥锁(如std::mutex
)或其他同步机制来实现。
- 回答:
cmake,怎么指定动态编译;
回答
在CMake中,指定动态编译(即生成动态链接库而不是静态链接库)通常涉及设置目标类型以及相关的编译和链接选项。以下是如何在CMake中指定动态编译的详细步骤:
-
设置目标类型:
- 使用
add_library
命令时,指定目标类型为SHARED
而不是STATIC
。 - 示例:
add_library(mylib SHARED mylib.cpp)
- 这将生成一个名为
libmylib.so
(在Linux上)或mylib.dll
(在Windows上)的动态链接库。
- 使用
-
配置编译选项:
- 可以通过
target_compile_options
或set(CMAKE_CXX_FLAGS ...)
来设置特定的编译选项,但这些通常不是直接用于指定动态或静态链接的。 - 然而,你可能需要确保编译选项与动态库的生成兼容,例如使用
-fPIC
(位置无关代码)在Linux上。 - 示例:
set_target_properties(mylib PROPERTIES COMPILE_FLAGS "-fPIC")
- 可以通过
-
配置链接选项:
- 动态链接库可能需要特定的链接器选项,这些可以通过
target_link_options
或修改CMAKE_SHARED_LINKER_FLAGS
来设置。 - 但在大多数情况下,CMake会自动处理这些选项。
- 动态链接库可能需要特定的链接器选项,这些可以通过
-
安装动态库:
- 使用
install
命令来指定动态库的安装路径。 - 示例:
install(TARGETS mylib LIBRARY DESTINATION /usr/lib)
- 使用
-
处理依赖:
- 确保所有动态库依赖的其他库也都是动态链接的,并且这些依赖在运行时是可用的。
- 这可能涉及设置
RPATH
或RUNPATH
,以便动态链接器可以找到这些依赖库。
面试官追问
-
追问1:如何在CMake中设置
RPATH
或RUNPATH
以确保动态库在运行时能找到其依赖?- 回答:可以使用
set_target_properties
来设置INSTALL_RPATH
或INSTALL_RPATH_USE_LINK_PATH
属性。INSTALL_RPATH
指定了运行时库搜索路径,而INSTALL_RPATH_USE_LINK_PATH
则会自动将链接时使用的路径添加到RPATH
中。
- 回答:可以使用
-
追问2:在Windows上生成动态链接库时,有哪些特定的CMake配置需要注意?
- 回答:在Windows上,你可能需要设置
CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS
为TRUE
,以便导出所有符号(除非你有选择性地导出符号)。此外,确保你的编译器和链接器选项与Windows动态链接库的生成兼容。
- 回答:在Windows上,你可能需要设置
-
追问3:如果我的项目同时需要生成静态库和动态库,如何在CMake中处理这种情况?
- 回答:你可以为同一个源代码文件添加多个
add_library
调用,但指定不同的目标类型(STATIC
和SHARED
)。然后,你可以根据需要为这两个库设置不同的属性。
- 回答:你可以为同一个源代码文件添加多个
-
追问4:在Linux上,如何确保动态链接库在运行时被正确加载,即使它们不在标准的库搜索路径中?
- 回答:除了设置
RPATH
或RUNPATH
之外,你还可以使用ldconfig
工具来更新系统的动态链接器缓存,或者通过设置环境变量LD_LIBRARY_PATH
来指定额外的库搜索路径。
- 回答:除了设置
-
追问5:CMake中是否有一种方法来自动检测系统上已安装的库,并链接到我的项目中?
- 回答:CMake的
find_package
命令可以用来查找和配置系统上已安装的库。你需要提供一个Find<PackageName>.cmake
模块(通常随库一起提供)或使用CMake的内置模块。一旦找到库,你可以使用target_link_libraries
来链接到你的目标中。
- 回答:CMake的
比如有程序编译成功,但是报错undefined,怎么处理;
问题:比如有程序编译成功,但是运行时报错 undefined
,怎么处理?
回答:
当程序编译成功但在运行时出现 undefined
错误(通常指未定义的行为或未定义引用错误),这通常表明在链接阶段缺少了某些定义或者代码中存在未初始化的变量使用等问题。以下是一系列步骤来解决这个问题:
-
检查错误信息:
- 首先,仔细阅读编译器和链接器提供的错误信息。错误信息通常会指出哪个符号(函数或变量)是未定义的。
-
检查代码:
- 确认所有使用到的函数和变量是否都已经被声明和定义。
- 对于函数,确保函数原型和定义匹配,并且函数定义是在使用它的文件之前或已经包含在了项目中。
- 对于变量,检查是否有未初始化的使用,尤其是全局变量或静态变量。
-
检查链接设置:
- 确保所有相关的源文件都已经包含在编译命令中。
- 如果使用了库文件(如
.lib
、.a
、.so
或.dll
),确保这些库文件已经被正确链接到项目中。 - 检查链接器的路径设置,确保能够找到所有需要的库文件。
-
检查外部依赖:
- 如果使用了第三方库,确保这些库是完整且兼容的。
- 检查是否有任何库版本冲突。
-
使用工具进行诊断:
- 使用
nm
工具(在 Unix-like 系统上)检查库文件是否包含所需的符号。 - 使用
dumpbin
(在 Windows 上)或otool
(在 macOS 上)来查看二进制文件的信息。 - 使用调试器(如 GDB 或 LLDB)运行程序,设置断点以检查在运行时哪些符号是未定义的。
- 使用
-
编译器和链接器选项:
- 检查编译器和链接器的命令行选项,确保没有误用或遗漏任何关键选项。
- 特别注意是否有任何优化选项影响了代码的行为(如
-O2
、-O3
等)。
-
重构和模块化:
- 如果项目较大,考虑重构代码,将其分解为更小的模块,每个模块更容易管理和调试。
- 使用现代 C++ 的特性(如智能指针、命名空间等)来减少命名冲突和未定义行为的可能性。
面试官追问及回答:
追问1:
- 问题:如果错误信息指向了一个库函数,而你确定库已经正确链接,还可能是什么原因?
回答:
- 这可能是因为库文件本身就有问题(如损坏或不兼容)。可以尝试重新下载或编译该库。
- 另外,也可能是链接到了错误的库版本。确保链接的是与项目兼容的库版本。
- 还有一种可能是库的依赖没有正确满足,比如库 A 依赖于库 B,但只链接了库 A 而没有链接库 B。
追问2:
- 问题:如果错误信息是在运行时动态加载的库中出现的,应该如何处理?
回答:
- 动态加载库(如使用
dlopen
在 Linux 上或LoadLibrary
在 Windows 上)时,需要确保库的路径正确,并且库中的所有依赖也都被正确加载。 - 使用
dlerror
(Linux)或GetLastError
(Windows)来获取更多关于加载失败的信息。 - 检查库的版本是否与动态加载时的环境兼容。
- 确保在动态加载库之前,所有必要的初始化代码已经执行。
追问3:
- 问题:如何避免在大型项目中频繁遇到这类链接错误?
回答:
- 使用构建系统(如 CMake、Makefile、Bazel 等)来管理项目的编译和链接过程,确保所有依赖都被正确处理。
- 定期进行代码审查和重构,确保代码的可维护性和清晰性。
- 编写自动化测试,包括单元测试、集成测试等,以在代码变更时及时发现潜在的问题。
- 使用静态分析工具(如 Clang-Tidy、Cppcheck)来检查代码中的潜在问题。
定位到有一个.so动态库,找不到一些符号链接,怎么处理;
问题:定位到有一个 .so
动态库,找不到一些符号链接,怎么处理?
回答:
当在嵌入式C++项目中遇到 动态库找不到一些符号链接的问题时,这通常意味着在运行时链接器
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
【C/C++面试必考必会】专栏,直击面试核心,精选C/C++及相关技术栈中面试官最爱的必考点!从基础语法到高级特性,从内存管理到多线程编程,再到算法与数据结构深度剖析,一网打尽。助你快速构建知识体系,轻松应对技术挑战。希望专栏能让你在面试中脱颖而出,成为技术岗的抢手人才。