GCC Compiler Option 2 : Preprocessor Options

-Mxxx

  • -M option is designed for auto-generate Makefile rules from g++ command.
  • 默认包含-E option to STOP after preprocessor during the compilation
  • 默认包含-w option to DISABLE/suppress all warnings.

Using a complex g++ command as an example:

1
2
g++ -Wall -Werror -Wno-unknown-pragmas -DPIN_CRT=1 -fno-stack-protector -fno-exceptions -funwind-tables -fasynchronous-unwind-tables -fno-rtti -DTARGET_IA32E -DHOST_IA32E -fPIC -DTARGET_LINUX -fabi-version=2 -faligned-new -I../../../source/include/pin -I../../../source/include/pin/gen -isystem /staff/shaojiemike/Download/pin-3.28-98749-g6643ecee5-gcc-linux/extras/cxx/include -isystem /staff/shaojiemike/Download/pin-3.28-98749-g6643ecee5-gcc-linux/extras/crt/include -isystem /staff/shaojiemike/Download/pin-3.28-98749-g6643ecee5-gcc-linux/extras/crt/include/arch-x86_64 -isystem /staff/shaojiemike/Download/pin-3.28-98749-g6643ecee5-gcc-linux/extras/crt/include/kernel/uapi -isystem /staff/shaojiemike/Download/pin-3.28-98749-g6643ecee5-gcc-linux/extras/crt/include/kernel/uapi/asm-x86 -I../../../extras/components/include -I../../../extras/xed-intel64/include/xed -I../../../source/tools/Utils -I../../../source/tools/InstLib -O3 -fomit-frame-pointer -fno-strict-aliasing -Wno-dangling-pointer 
-M inscount0.cpp -o Makefile_bk

In Makefile_bk

1
2
3
4
5
6
7
8
9
inscount0.o: inscount0.cpp \
# sys header
/usr/include/stdc-predef.h \
/staff/shaojiemike/Download/pin-3.28-98749-g6643ecee5-gcc-linux/extras/cxx/include/iostream \
/usr/lib/gcc/x86_64-linux-gnu/11/include/float.h
# usr header
../../../source/include/pin/pin.H \
../../../extras/xed-intel64/include/xed/xed-interface.h \
... more header files
  • -MM not include sys header file
    • e.g., the first 3 header will be disapear.
  • -MF filename config the Makefile rules write to which file instead of to stdout.
  • -M -MG is designed to generate Makefile rules when there is header file missing, treated it as generated in normal.
  • -M -MP will generated M-rules for dependency between header files
    • e.g., header1.h includes header2.h. So header1.h: header2.h in Makefile
  • -MD == -M -MF file without default option -E
    • the default file has a suffix of .d, e.g., inscount0.d for -c inscount0.cpp
  • -MMD == -MD not include sys header file

需要进一步的研究学习

暂无

遇到的问题

暂无

开题缘由、总结、反思、吐槽~~

参考文献

上面回答部分来自ChatGPT-3.5,没有进行正确性的交叉校验。

C++ ABI

摘要

ABI被人熟知,就是编译时,接口不匹配导致运行时的动态库undefined symbol报错。

Read more

Zsim

简介

Zsim模拟器是

  • 一个快速的x86-64多核模拟器,它利用英特尔Pin工具包收集进程的内存访问跟踪,并在zsim模拟器中重放跟踪。
  • 一种基于 PIN 的二进制插桩工具,可以在运行时对指令进行插桩,并插入自定义的函数调用,用于模拟内存子系统的行为。
  • 它最初是为了评估ZCache而编写的,但后来它扩展了其功能
  • zsim的主要目标是快速,简单和准确,重点是模拟内存层次结构和大型异构系统
Read more

Intel Pin

简介

Pin 是一个动态二进制插桩工具:

  • 支持 Linux, macOS 和 Windows 操作系统以及可执行程序。
  • Pin可以通过pintools在程序运行期间动态地向可执行文件的任意位置插入任意代码(C/C++),也可以attach到一个正在运行的进程。
  • Pin 提供了丰富的API,可以抽象出底层指令集特性,并允许将进程的寄存器数据等的上下文信息作为参数传递给注入的代码。Pin会自动存储和重置被注入代码覆盖的寄存器,以恢复程序的继续运行。对符号和调试信息也可以设置访问权限。
  • Pin内置了大量的样例插桩工具的源码,包括基本块分析器、缓存模拟器、指令跟踪生成器等,根据自己的实际需求进行自定义开发也十分方便。

特点

  • 只是它的输入不是字节码,而是可执行文件的执行汇编代码(机械码)。
  • Pin 对所有实际执行的代码进行插桩,但是如果指令没有被执行过,就一定不会被插桩。
  • 工作在操作系统之上,所以只能捕获用户级别的指令。
  • 由于Pintool的编译十分自由,Pin不打算提供多线程插桩的支持。
  • 同时有3个程序运行:应用程序本身、Pin、Pintool。
    • 应用程序是被插桩的对象、Pintool包含了如何插桩的规则(用户通过API编写的插入位置和内容)
    • 三者共享同一个地址空间,但不共享库,避免了冲突。 Pintool 可以访问可执行文件的全部数据,还会与可执行文件共享 fd 和其他进程信息。

pin

基本原理

Pin机制类似Just-In-Time (JIT) 编译器,Trace插桩的基本流程(以动态基本块BBLs为分析单位):

  • Pin 会拦截可执行文件的第一条指令,然后对从该指令开始的后续的指令序列重新“compile”新的代码,并执行
  • Pin 在分支退出代码序列时重新获得控制权限,基于分支生成更多的代码,然后继续运行。

动态基本块BBLs

通过一个例子来说明动态基本块BBLs与 汇编代码的BB的区别

1
2
3
4
5
6
7
8
9
switch(i)
{
case 4: total++;
case 3: total++;
case 2: total++;
case 1: total++;
case 0:
default: break;
}

上述代码会编译成下面的汇编, 对于实际执行时跳转从.L7进入的情况,BBLs包括四条指令,但是BB只会包括一条。

1
2
3
4
5
6
7
8
.L7:
addl $1, -4(%ebp)
.L6:
addl $1, -4(%ebp)
.L5:
addl $1, -4(%ebp)
.L4:
addl $1, -4(%ebp)

Pin会将cpuid, popf and REP prefixed 指令在执行break 成很多BBLs,导致执行的基本块比预想的要多。(主要原因是这些指令有隐式循环,所以Pin会将其拆分成多个BBLs)

Download

  1. Download the kit from Intel website
  2. Because the compatibility problem may you should install pin with archlinux package

Installation

This part is always needed by pintool, for example Zsim, Sniper.

When you meet the following situation, you should consider update your pin version even you can ignore this warning by use flags like -ifeellucky under high compatibility risk.

1
2
3
shaojiemike@snode6 ~/github/ramulator-pim/zsim-ramulator/pin  [08:05:47]
> ./pin
E: 5.4 is not a supported linux release

because this will easily lead to the problem

1
Pin app terminated abnormally due to signal 6. # or signal 4.

Install pintool(zsim) by reconfig pin version

  1. My first idea is try a compatible pin version (passd a simple test pintool, whatever) instead of the old pin.
    1. Find the suitable simpler pintool can reproduce the situation (old pin failed, but newest pin is passed)
      1. TODO: build(fix pin2.14 CXX_ABI compatibility bug), test suitability
  2. debug the pin tool in details (See in another blog)

插桩粒度

  • Trace instrumentation mode:以动态基本块BBLs为分析单位
  • Instruction instrumentation mode:以指令为分析单位
  • 其余粒度/角度,这些模式通过“缓存”插装请求来实现,因此会产生空间开销,这些模式也被称为预先插装
    • Image instrumentation mode: 通过前提知道需要执行的所有代码,能绘制出全局的插桩情况图
    • Routine instrumentation mode: 理解为函数级插桩,可以对目标程序所调用的函数进行遍历,并在每一个routine中对instruction做更细粒度的遍历。
    • 两者都是在IMG加载时提前插桩,所有不一定routine会被执行。

范围image, section, routine, trace, basic block, instruction的含义和运行时关系

  • image包含section,section包含routine(理由:SEC_Img()RTN_Sec(),后面的静态遍历IMG中指令条数的代码也能说明)
  • routine指的是程序中的函数或子程序,trace指的是程序执行过程中的一条路径,basic block指的是一段连续的指令序列,其中没有跳转指令,instruction指的是程序中的一条指令。
  • 在程序执行时,一个routine可以包含多个trace,一个trace可以包含多个basic block,一个basic block可以包含多个instruction。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for (SEC sec = IMG_SecHead(img); SEC_Valid(sec); sec = SEC_Next(sec))
{
for (RTN rtn = SEC_RtnHead(sec); RTN_Valid(rtn); rtn = RTN_Next(rtn))
{
// Prepare for processing of RTN, an RTN is not broken up into BBLs,
// it is merely a sequence of INSs
RTN_Open(rtn);

for (INS ins = RTN_InsHead(rtn); INS_Valid(ins); ins = INS_Next(ins))
{
count++;
}

// to preserve space, release data associated with RTN after we have processed it
RTN_Close(rtn);
}
}

API

最重要的是

  • 插桩机制(instrumentation code):插入位置,在什么位置插入什么样的代码
  • 分析代码(analysis code):插入内容,在插桩点执行的代码 (和上一点的区别是什么???)

插桩机制(instrumentation code)

  • TRACE_AddInstrumentFunction Add a function used to instrument at trace granularity
    • BBL粒度插桩相关API
  • INS_AddInstrumentFunction() Add a function used to instrument at instruction granularity
    • 指令粒度插桩相关API
  • IMG_AddInstrumentFunction() Use this to register a call back to catch the loading of an image
  • 插桩不仅可以对每个指令插桩,还可以通过分类筛选后,只对符合要求的指令进行插桩
    • 比如,使用INS_InsertPredicatedCall()

遍历所有的指令

1
2
3
4
5
// Forward pass over all instructions in bbl
for( INS ins= BBL_InsHead(bbl); INS_Valid(ins); ins = INS_Next(ins) )

// Forward pass over all instructions in routine
for( INS ins= RTN_InsHead(rtn); INS_Valid(ins); ins = INS_Next(ins) )

遍历trace内BBLs

1
2
3
4
5
6
// Visit every basic block  in the trace
for (BBL bbl = TRACE_BblHead(trace); BBL_Valid(bbl); bbl = BBL_Next(bbl))
{
// Insert a call to docount before every bbl, passing the number of instructions
BBL_InsertCall(bbl, IPOINT_BEFORE, (AFUNPTR)docount, IARG_UINT32, BBL_NumIns(bbl), IARG_END);
}

遍历指令里的memOperands

1
2
3
4
5
6
7
UINT32 memOperands = INS_MemoryOperandCount(ins);

// Iterate over each memory operand of the instruction.
for (UINT32 memOp = 0; memOp < memOperands; memOp++){
if (INS_MemoryOperandIsRead(ins, memOp)||INS_MemoryOperandIsWritten(ins, memOp)
//xxx
}

Pintool

最重要的是

  • 插桩机制(instrumentation code):插入位置,在什么位置插入什么样的代码
  • 分析代码(analysis code):插入内容,在插桩点执行的代码 (和上一点的区别是什么???)

Pintool代码

示例分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// IPOINT_BEFORE 时运行的分析函数
VOID printip(VOID* ip) { fprintf(trace, "%p\n", ip); }

// Pin calls this function every time a new instruction is encountered
VOID InstructionFuc(INS ins, VOID* v)
{
// Insert a call to printip before every instruction, and pass it the IP
// IARG_INST_PTR:指令地址 一类的全局变量???
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)printip, IARG_INST_PTR, IARG_END);
}

int main(int argc, char* argv[])
{
// Initialize pin
if (PIN_Init(argc, argv)) return Usage();

// 登记InstructionFuc为以指令粒度插桩时每条指令触发的函数
INS_AddInstrumentFunction(InstructionFuc, 0);

// 登记PrintFuc为程序结束时触发的函数
PIN_AddFiniFunction(PrintFuc, 0);

// 部署好触发机制后开始运行程序
PIN_StartProgram();

return 0;
}

Debug pintool

目标:以样例插桩工具的源码为对象,熟悉pin的debug流程。

官方教程为例子:

1
2
3
4
5
6
7
uname -a #intel64
cd source/tools/ManualExamples
# source/tools/Config/makefile.config list all make option
make all OPT=-O0 DEBUG=1 TARGET=intel64 |tee make.log|my_hl
# or just select one: make obj-intel64/inscount0.so
# $(OBJDIR)%$(PINTOOL_SUFFIX) - Default rule for building tools.
# Example: make obj-intel64/mytool.so

测试运行

1
../../../pin -t obj-intel64/inscount0.so -- ./a.out #正常统计指令数 to inscount.out

下面介绍Pin 提供的debug工具:

首先创建所需的-gstack-debugger.so和应用fibonacci.exe

1
2
cd source/tools/ManualExamples
make OPT=-O0 DEBUG=1 stack-debugger.test

其中OPT=-O0选项来自官方文档Using Fast Call Linkages小节,说明需要OPT=-O0选项来屏蔽makefile中的-fomit-frame-pointer选项,使得GDB能正常显示stack trace(函数堆栈?)

Debug application in Pin JIT mode

1
2
3
4
$ ../../../pin -appdebug -t obj-intel64/stack-debugger.so -- obj-intel64/fibonacci.exe 1000
Application stopped until continued from debugger.
Start GDB, then issue this command at the prompt:
target remote :33030

使用pin的-appdebug选项,在程序第一条指令前暂停,并启动debugger窗口。在另一个窗口里gdb通过pid连接:

1
2
3
4
$ gdb fibonacci #如果没指定应用obj-intel64/fibonacci.exe
(gdb) target remote :33030 #连接gdb端口
(gdb) file obj-intel64/fibonacci.exe #如果没指定应用, 需要指定程序来加载symbols
(gdb) b main #continue 等正常操作

Pintool添加自定义debug指令

能够在上一小节的debug窗口里,通过自定义debug指令打印自定义程序相关信息(比如当前stack使用大小)

Pintool设置满足条件时break并启动debug窗口

Pintool “stack-debugger” 能够监控每条分配stack空间的指令,并当stack使用达到阈值时stop at a breakpoint。

这功能由两部分代码实现,一个是插桩代码,一个是分析代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static VOID Instruction(INS ins, VOID *)
{
if (!EnableInstrumentation) // ROI(Region of interest)开始插桩测量
return;

if (INS_RegWContain(ins, REG_STACK_PTR)) //判断指令是不是会改变stack指针(allocate stack)
{
IPOINT where = IPOINT_AFTER;
if (!INS_IsValidForIpointAfter(ins))
where = IPOINT_TAKEN_BRANCH; //寻找stack空间判断函数插入位置(指令执行完的位置)。如果不是after, 就是taken branch

INS_InsertIfCall(ins, where, (AFUNPTR)OnStackChangeIf, IARG_REG_VALUE, REG_STACK_PTR,
IARG_REG_VALUE, RegTinfo, IARG_END); // 插入OnStackChangeIf函数,如果OnStackChangeIf()返回non-zero, 执行下面的DoBreakpoint函数
INS_InsertThenCall(ins, where, (AFUNPTR)DoBreakpoint, IARG_CONST_CONTEXT, IARG_THREAD_ID, IARG_END);
}
}

所需的两个函数的分析代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
static ADDRINT OnStackChangeIf(ADDRINT sp, ADDRINT addrInfo)
{
TINFO *tinfo = reinterpret_cast<TINFO *>(addrInfo);

// The stack pointer may go above the base slightly. (For example, the application's dynamic
// loader does this briefly during start-up.)
//
if (sp > tinfo->_stackBase)
return 0;

// Keep track of the maximum stack usage.
//
size_t size = tinfo->_stackBase - sp;
if (size > tinfo->_max)
tinfo->_max = size; //更新stack使用大小

// See if we need to trigger a breakpoint.
//
if (BreakOnNewMax && size > tinfo->_maxReported)
return 1;
if (BreakOnSize && size >= BreakOnSize)
return 1;
return 0;
}

static VOID DoBreakpoint(const CONTEXT *ctxt, THREADID tid)
{
TINFO *tinfo = reinterpret_cast<TINFO *>(PIN_GetContextReg(ctxt, RegTinfo));

// Keep track of the maximum reported stack usage for "stackbreak newmax".
//
size_t size = tinfo->_stackBase - PIN_GetContextReg(ctxt, REG_STACK_PTR);
if (size > tinfo->_maxReported)
tinfo->_maxReported = size;

ConnectDebugger(); // Ask the user to connect a debugger, if it is not already connected.

// Construct a string that the debugger will print when it stops. If a debugger is
// not connected, no breakpoint is triggered and execution resumes immediately.
//
tinfo->_os.str("");
tinfo->_os << "Thread " << std::dec << tid << " uses " << size << " bytes of stack.";
PIN_ApplicationBreakpoint(ctxt, tid, FALSE, tinfo->_os.str());
}

OnStackChangeIf函数监控当前的stack使用并判断是否到达阈值。DoBreakpoint函数连接debugger窗口,然后触发breakpoint,并打印相关信息。

也可以使用-appdebug_enable参数,取消在第一条指令前开启GDB窗口的功能,而是在触发如上代码的break时,才开启GDB窗口的连接。

而上述代码中的ConnectDebugger函数实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void ConnectDebugger()
{
if (PIN_GetDebugStatus() != DEBUG_STATUS_UNCONNECTED) //判断是不是已有debugger连接
return;

DEBUG_CONNECTION_INFO info;
if (!PIN_GetDebugConnectionInfo(&info) || info._type != DEBUG_CONNECTION_TYPE_TCP_SERVER) //PIN_GetDebugConnectionInfo()获取GDB所需的tcp连接端口
return;

*Output << "Triggered stack-limit breakpoint.\n";
*Output << "Start GDB and enter this command:\n";
*Output << " target remote :" << std::dec << info._tcpServer._tcpPort << "\n";
*Output << std::flush;

if (PIN_WaitForDebuggerToConnect(1000*KnobTimeout.Value())) //等待其余GDB窗口的连接
return;

*Output << "No debugger attached after " << KnobTimeout.Value() << " seconds.\n";
*Output << "Resuming application without stopping.\n";
*Output << std::flush;
}

Tips for Debugging a Pintool

这部分讲述了如何debug Pintool中的问题。(对Pintool的原理也能更了解

为此,pin使用了-pause_tool n 暂停n秒等待gdb连接。

1
2
3
4
5
6
7
8
../../../pin -pause_tool 10 -t /staff/shaojiemike/github/sniper_PIMProf/pin_kit/source/tools/ManualExamples/obj-intel64/stack-debugger.so -- obj-intel64/fibonacci.exe 1000
Pausing for 10 seconds to attach to process with pid 3502000
To load the debug info to gdb use:
*****************************************************************
set sysroot /not/existing/dir
file
add-symbol-file /staff/shaojiemike/github/sniper_PIMProf/pin_kit/source/tools/ManualExamples/obj-intel64/stack-debugger.so 0x7f3105f24170 -s .data 0x7f31061288a0 -s .bss 0x7f3106129280
*****************************************************************

注意gdb对象既不是pin也不是stack-debugger.so,而是intel64/bin/pinbin。原因是intel64/bin/pinbinpin执行时的核心程序,通过htop监控可以看出。

1
2
3
# shaojiemike @ snode6 in ~/github/sniper_PIMProf/pin_kit/source/tools/ManualExamples on git:dev x [19:57:26]
$ gdb ../../../intel64/bin/pinbin
(gdb) attach 3502000

这时GDB缺少了stack-debugger.so的调试信息,需要手动添加。这里的add-symbol-file命令是在pin启动时打印出来的,直接复制粘贴即可。

1
2
3
4
5
6
7
8
9
(gdb) add-symbol-file /staff/shaojiemike/github/sniper_PIMProf/pin_kit/source/tools/ManualExamples/obj-intel64/stack-debugger.so 0x7f3105f24170 -s .data 0x7f31061288a0 -s .bss 0x7f3106129280
(gdb) b main #或者 b stack-debugger.cpp:94
gef➤ info b
Num Type Disp Enb Address What
1 breakpoint keep y <MULTIPLE>
1.1 y 0x00000000000f4460 <main> # 无法访问的地址,需要去除
1.2 y 0x00007f3105f36b65 in main(int, char**) at stack-debugger.cpp:94
(gdb) del 1.1
(gdb) c

个人尝试: 使用VSCODE调试Pintool

  • 想法:VSCODE的GDB也可以attach PID,理论上是可以的
  • 实际问题:VSCODE attach pid后,不会stopAtEntry,只会在已经设置好的断点暂停。但是无法访问到stack-debugger.so的调试信息,无法设置断点。

构建Pintool

  • 首先需要熟悉API
  • PinTool 编译需要自身的 Pin CRT(C RunTime)库,这个库是 Pin 提供的,可以在 Pin 安装目录下找到。

需要进一步的研究学习

暂无

遇到的问题

暂无

开题缘由、总结、反思、吐槽~~

参考文献

https://paper.seebug.org/1742/

https://software.intel.com/sites/landingpage/pintool/docs/98690/Pin/doc/html/index.html#APPDEBUG_UNIX

Colorful Commands

导言

电脑玩家经常说RGB是最重要的,对于程序员来说,彩色的terminal有助于快速的分辨输出的有效信息。为此有一些有意思的彩色输出命令。

Cheat sheet (命令行小抄/备忘录)

最经典 tdlr

使用时需要网络

1
2
npm install -g tldr     
pip3 install tldr

tealdeer - simplest example

a RUST fast version oftdlr

Tips: OpenSSL development headers
get a “failed to run custom build command for openssl-sys” error message. The package is calledlibssl-dev on Ubuntu.

1
2
3
4
5
# install
cargo install tealdeer

# 使用
tdlr <>

部分支持中文,支持多平台

支持在线网页版

  1. Linux Command Line Cheat Sheet
  2. devhints.io
    1. 各种的选项,不止命令,包括bash, vim, mysql, git
    2. 语言 go, python, java, JS, NodeJS, Ruby 3.
  3. navi
    1. 支持语意转换的补全 🔥
  4. cheat.sh 🔥
    1. 支持 curl命令直接访问或者交互式
    2. 支持补全
    3. 返回内容集成cheat cheat.sheets tdlr
  5. bropages.org
    1. 用户自发投票排序的命令用例

navi installation & usage

1
2
3
4
5
6
7
8
9
# install
cargo install --locked navi

# download default cheatsheet
navi repo add denisidoro/cheats

# 使用
navi
# 基于fzf寻找需要指令

SHELL

awesome-shell里多看看。

set_tsj.sh

1
2
3
4
5
6
HOME=/home/t00906153
export SHELL=zsh
export HOME=$HOME
export HISTFILE=$HOME/.zsh_history
cd
zsh

oh my zsh

虽然这个ohmyzsh好用,但是我用惯了hyq的模板, 从github上下载后解压就安装了zsh模板。(之后可以考虑传到云盘或者cloudflare)

1
2
3
4
5
HOME=/home/xxx
export SHELL=zsh
export PATH=$HOME/.local/bin:$PATH
export HISTFILE=$HOME/.zsh_history
zsh
1
2
3
4
# git clone https://github.com/Kirrito-k423/QuickStartLinux.git --depth=1
# cd QuickStartLinux/resources
wget https://raw.githubusercontent.com/Kirrito-k423/QuickStartLinux/main/resources/zsh.tar
tar xvf zsh.tar -C $HOME

oh my zsh

有时候hyq模版有些兼容性问题,比较老的zsh不支持(大约是5.0.2到5.5.1的版本)。这时候只能手动安装了,包括主题和插件。请看zsh一文。

zsh_history 支持多端口同步,实时保存

title
1
2
3
4
5
6
7
8
9
HISTFILE=~/.zsh_history
SAVEHIST=30000 # 最多 10000 条命令的历史记录
setopt APPEND_HISTORY # 退出 zsh 会话时,命令不会被覆盖式地写入到历史文件,而是追加到该文件的末尾
setopt INC_APPEND_HISTORY # 会话进行中也会将命令追加到历史文件中
setopt SHARE_HISTORY # 所有会话中输入的命令保存到一个共享的历史列表中
export HIST_SAVE_FREQ=10 # 每10次命令保存一次
ulimit -c unlimited
echo '$HOME/core-%e.%p.%h.%t' > /proc/sys/kernel/core_pattern
cd ~

窗口管理器 tmux-like

大部分Linux 系统自带的 screen 命令来多终端控制

外部控制:

  • 创建: screen -S name
  • 恢复: screen -r name
  • 列表: screen -ls
  • 删除: screen -X -S name kill

内部控制:

  • CtrlA + d 分离
  • CtrlA + x 终端上锁,CtrlA + k 是kill
  • CtrlA + S 上下分屏,CtrlA + | 左右分屏。
    • screen -S name创建会话后,需要在新窗口中执行screen来创建额外的终端;这样分屏之后才有两个以上的终端可以使用。
    • CtrlA + w 查看终端
    • CtrlA + [数字] 切换到第几个
    • CtrlA + Tab 返回上一个
    • CtrlA + X 关闭分屏
    • CtrlA + : 进入命令模式,输入resize -v 100或者resize -h 100调整大小。

oh my tmux 🔥

1
2
3
4
5
6
7
8
9
10
11
12
## Install
cd
git clone https://github.com/gpakosz/.tmux.git
ln -s -f .tmux/.tmux.conf
cp .tmux/.tmux.conf.local .

# or
cd ~/resources
# wget https://gitee.com/shaojiemike/oh-my-tmux/repository/blazearchive/master.zip?Expires=1629202041&Signature=Iiolnv2jN6GZM0hBWY09QZAYYPizWCutAMAkhd%2Bwp%2Fo%3D
unzip oh-my-tmux-master.zip -d ~/
ln -s -f ~/oh-my-tmux-master/.tmux.conf ~/.tmux.conf
cp ~/oh-my-tmux-master/.tmux.conf.local ~/.tmux.conf.local

基于Rust的zellij

开发编辑器 vim-like

vimrc 🔥

1
2
git clone --depth=1 https://github.com/amix/vimrc.git ~/.vim_runtime
sh ~/.vim_runtime/install_awesome_vimrc.sh

emacs

针对不同语言有许多可选插件

Ubuntu install emacs27

1
2
3
add-apt-repository ppa:kelleyk/emacs
apt-get update
apt-get install emacs27

问题:

1
2
3
4
dpkg-deb: error: paste subprocess was killed by signal (Broken pipe)
Errors were encountered while processing:
/var/cache/apt/archives/emacs27-common_27.1~1.git86d8d76aa3-kk2+20.04_all.deb
E: Sub-process /usr/bin/dpkg returned an error code (1)

版本解决,强制安装 sudo apt-get -o Dpkg::Options::="--force-overwrite" install emacs27-common

1
2
3
4
5
6
7
sudo apt --purge remove emacs27
sudo apt --purge remove emacs
sudo apt --purge remove emacs-common
sudo apt --fix-broken install
sudo apt autoremove
sudo apt install emacs27
emacs --version

常用命令的”Colorful”版本

cd

z.lua - learning cd 🔥

Install LUA

1
2
3
4
5
curl -R -O http://www.lua.org/ftp/lua-5.4.4.tar.gz       
tar zxf lua-5.4.4.tar.gz
cd lua-5.4.4
make all test
sudo make install # usr/bin

Install

1
2
3
4
5
6
7
8
9
10
cd ~/github
git clone https://github.com/skywind3000/z.lua.git

# vim ~/.zshrc
alias zz='z -c' # 严格匹配当前路径的子路径
alias zi='z -i' # 使用交互式选择模式
alias zf='z -I' # 使用 fzf 对多个结果进行选择
alias zb='z -b' # 快速回到父目录
#eval "$(lua /path/to/z.lua --init zsh)" # ZSH 初始化
eval "$(lua ~/github/z.lua/z.lua --init zsh)"

常用命令

1
2
3
4
5
6
7
8
9
10
11
12
# 弹出栈顶 (cd 到上一次的老路径),和 "z -0" 相同
$ z -

# 显示当前的 dir stack
$ z --

# 交互式
z -i foo # 进入交互式选择模式,让你自己挑选去哪里(多个结果的话)
z -I foo # 进入交互式选择模式,但是使用 fzf 来选择

# 匹配
z foo$
1
2
3
4
5
6
7
8
9
10
z foo       # 跳转到包含 foo 并且权重(Frecent)最高的路径       
z foo bar # 跳转到同时包含 foo 和 bar 并且权重最高的路径
z -r foo # 跳转到包含 foo 并且访问次数最高的路径
z -t foo # 跳转到包含 foo 并且最近访问过的路径
z -l foo # 不跳转,只是列出所有匹配 foo 的路径
z -c foo # 跳转到包含 foo 并且是当前路径的子路径的权重最高的路径
z -e foo # 不跳转,只是打印出匹配 foo 并且权重最高的路径
z -i foo # 进入交互式选择模式,让你自己挑选去哪里(多个结果的话)
z -I foo # 进入交互式选择模式,但是使用 fzf 来选择
z -b foo # 跳转到父目录中名称以 foo 开头的那一级

缺点:

  • 没去过的路径,每级文件夹的补全没有了
  • 可以和cd结合使用

ls

exa 🔥 - better ls

1
2
3
4
5
6
7
8
9
# Manual installation from GitHub. Ubuntu 20.10才支持
wget https://github.com/ogham/exa/releases/download/v0.10.1/exa-linux-x86_64-musl-v0.10.1.zip
unzip exa-linux-x86_64-musl-v0.10.1.zip
mv bin/exa ~/.local/bin

# 使用
exa -l
# 文件夹大小
du -d 1 -h .

grep

rg (Fast & Good multi-platform compatibility) > ag > ack(ack-grep) 🔥

[repgrep(rg)](https

1
2
3
4
5
6
# ripgrep(rg) 但是readme说这样有bugs       
sudo apt-get install ripgrep
# 可执行文件 (推荐)
wget https://github.com/BurntSushi/ripgrep/releases/download/13.0.0/ripgrep-13.0.0-x86_64-unknown-linux-musl.tar.gz
tar -zxvf ripgrep-13.0.0-x86_64-unknown-linux-musl.tar.gz
mv ./ripgrep-13.0.0-x86_64-unknown-linux-musl/rg ~/.local/bin

repgrep(rg) 常用选项

  • --no-ignore 忽略.gitignore之类的文件,也搜索忽略的文件。(默认是不搜索的)
  • -t txt 指定搜索类型
  • rg 'content' ABC/*.cpp搜索和正则ABC/*.cpp匹配的文件

ag

1
2
3
4
# ag 2020年就不维护了       
apt-get install silversearcher-ag
# It ignores file patterns from your .gitignore and .hgignore.
# use -u or -U option to reinclude these files to search scoop

find

find . -name "*xxx*"

fzf 🔥带预览的find

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ubuntu
sudo apt install fzf

# GIT install
git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf
~/.fzf/install
source ~/.zshrc
# Vim-plugin
Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }

# 使用
fzf --preview 'less {}'

# 安装了bat
fzf --preview "batcat --style=numbers --color=always --line-range :500 {}"

telescope.nvim 也带预览的find

官网

1
2
3
4
5
6
7
8
9
10
# 先安装vim-plug
curl -fLo~/.vim/autoload/plug.vim --create-dirs \ https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim
# 修改~/.vimrc
call plug#begin()
Plug 'nvim-lua/plenary.nvim'
Plug 'nvim-telescope/telescope.nvim'
call plug#end()

# 还需要 Neovim
to do

cat

bat 🔥 - colorful cat

1
2
3
4
5
6
7
8
# Install
sudo apt install bat

# 使用
batcat filename
# 指定行号
alias cat="batcat"
cat -r 35:42 /etc/hosts

git

gitui 🔥 - fast Rust lazygit

https://github.com/extrawurst/gitui/releases

1
2
3
4
5
6
7
8
9
# 建议Rust,三句命令,安装Rust,source,gitui
curl https://sh.rustup.rs -sSf | sh
source $HOME/.cargo/env
cargo install gitui

# 安装(由于还在开发中建议去官网 , 现在不支持armV7
wget https://github.com/extrawurst/gitui/releases/download/v0.20.1/gitui-linux-musl.tar.gz
tar -zxvf gitui-linux-musl.tar.gz
mv gitui ~/.local/bin

lazygit

1
2
3
4
5
6
# install go
wget https://go.dev/dl/go1.18.1.linux-amd64.tar.gz

git clone https://github.com/jesseduffield/lazygit.git
cd lazygit
go install

高亮终端输出/log文件

bash脚本输出颜色文本示例

1
2
3
4
5
RED='\033[0;31m'       
NC='\033[0m'
# No Color
printf "I ${RED}love${NC} Stack Overflow\n"
echo -e "\033[5;36m Orz 旧容器(镜像)已清理\033[0m"

颜色编号如下

颜色 编号
Black 0;30
Dark Gray 1;30
Red 0;31
Light Red 1;31
Green 0;32
Light Green 1;32
Brown/Orange 0;33
Yellow 1;33
Blue 0;34
Light Blue 1;34
Purple 0;35
Light Purple 1;35
Cyan 0;36
Blue 0;37
Light Cyan 1;36
Light Gray 0;37
White 1;37

hl 🔥自定义高亮各种log文件

通过regular expressions自定义高亮各种log文件

install需要 lex

1
2
3
4
5
git clone https://github.com/mbornet-hl/hl       
make clean;
make
cp hl /usr/local/bin
# move

颜色支持(3浅中深 * 6颜色 * 背景色反转)

1
2
3
# 前面 123 是深浅 , 4是下划线       
# 字母大写是背景色反转
-r : red -g : green -y : yellow -b : blue -m : magenta -c : cyan -w : white

正则标记log关键词

绿色和红色

1
cat exemple.log | hl -g start -r stop       

正则表示

1
2
3
4
5
6
7
-e : extended regular expressions
-i : ignore case

hl -ei -g '(start(|ing|ed))' -r '(stop(|ping|ped))'

## ip 匹配
curl --trace-ascii - www.baidu.com|hl -ei -1R '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'

命令配置文件 hl_ha.cfg

默认设置

1
2
export HL_CONF=/staff/shaojiemike/github/hl/config_files       
echo $HL_CONF │/staff/shaojiemike/github/hl/config_files
1
2
-%c : specifies the beginning of a range colorized in color 'c'       
-. : specifies the end of the previous range

Colorize hl configurations :

1
hl -vop '*' | hl --hl_conf       

example Commands

1
2
3
4
5
lD # ls by date       
lW # ls by week
ifconfig -a | hl --ifconfig
# ping tcpdump fdisk apt-get diff
# ip ibstat iptables passwd

errors

常用方式

~/.zshrc 里如下配置:

1
2
3
4
5
6
export HL_CONF=/home/shaojiemike/github/hl/config_files       
function my_hl(){ hl -eg '\$\{?[A-Z_]+\}?' -ec ' ([A-Z_-]+) ' -eic '(nothing|note)' -eiy ' (-([a-z_-]+))' -eiy '0x[0-9a-z]+' --errors -eig '(yes)' -eir '((^| )no($| ))|(none)|(not)|(null)|(please)|( id )' -ir error -ir wrong -ib '(line)|(file)' -eiy '(warn(|ing))|(wait)|(idle)|(skip)' -im return -ic '(checking)' -eiy ' (__(.*)__) ' -ei1W '((\w*/[.A-Za-z0-9_/-]*[A-Za-z0-9_/-]*)("|$)?)|((\w*/[.A-Za-z0-9_/-]*[A-Za-z0-9_/-]*)(")? ) ' -3B '[0-9][0-9.]+' -3B ' ([0-9])|(#[0-9]+)' -eig '(start(|ing))' -eir '(end(|ing))' }
alias ifconfig='ifconfig | hl --ifconfig'
alias ip='ip a|hl --ip '
alias df='df -h |hl --df'
alias ibstat='ibstat |hl --ibstat'

编译时如此使用make 2>&1|my_hl

系统信息

dua-cli - best disk space viewer 🔥

资源监控

资源监控软件netdata

  • netdata 默认挂载在http://127.0.0.1:19999/。想要WebUI运行 sudo netdata -i node5.acsalab.com
  • cpu, disk, memory, network,温度都有记录
  • arm下有问题,需要自己编译

资源监控命令bottom(htop like)

1
2
3
4
5
6
# install
curl -LO https://github.com/ClementTsang/bottom/releases/download/0.6.8/bottom_0.6.8_amd64.deb
sudo dpkg -i bottom_0.6.8_amd64.deb

# 使用
btm

类似s-tui可以观察CPU 温度,频率

网络监控 bmon

bmon是类 Unix 系统中一个基于文本,简单但非常强大的网络监视和调试工具

Compile yourself

Install libconfuse

sh wget https://github.com/martinh/libconfuse/releases/download/v2.8/confuse-2.8.zip unzip confuse-2.8.zip && cd confuse-2.8 PATH=/usr/local/opt/gettext/bin:$PATH ./configure make make install Install bmon

sh git clone https://github.com/tgraf/bmon.git cd bmon ./autogen.sh ./configure make make install bmon

文件管理器

ranger 基于vi的支持预览的横向多级显示 🔥

https://github.com/ranger/ranger

1
2
3
pip install ranger-fm

# renger直接使用,方向键或者hjkl,可以直接跳转到vim修改

参考文献

[C++ Basic] Grammar

概要

C++ 基础知识和语法,包括C++11,C++17,C++23的各种语言支持。

C 与 C++、java 的 区别

支持范式模板编程 (generic programming)

  1. 模板代码(增加泛型编程能力,类似python),
    1. 泛型编程是一种以通用性为中心的编程范式。在泛型编程中,程序通过使用参数化类型(或称为模板)来实现数据类型无关的算法和数据结构
  2. 强⼤的 Standard Template Library (STL) 标准库, 也是基于泛型编程的产物。
    1. 包括:容器、迭代器、算法和函数对象 四类
  3. 元编程(e.g., constexpr )编译时推导出变量是否为固定常数。
  4. 一些语法和关键字,增加了 new 和 delete,auto

支持面向对象编程 (object-oriented programming) 的拓展

  • 类和对象:C++允许定义类和创建对象。类是一种用户自定义的数据类型,可以包含成员变量和成员函数。对象是类的一个实例,可以通过类来创建多个对象。C语言中没有类和对象的概念,只能使用结构体和函数来组织数据和行为。
  • 封装:C++支持封装,可以将数据和相关的操作封装在一个类中,并使用访问修饰符来控制对类成员的访问权限。C语言没有封装的概念,所有的数据和函数都是公开的。
  • 继承:C++支持继承,允许创建派生类从基类继承属性和行为。继承可以实现代码重用和类的层次化。C语言没有继承的概念。
  • 多态:C++支持多态,允许通过基类指针或引用来调用派生类的虚函数,实现动态绑定和运行时多态性。C语言没有多态的概念。
  • 异常处理:C++提供异常处理机制,可以通过抛出和捕获异常来处理程序中的错误和异常情况。C语言没有内置的异常处理机制。

C++ 与 java 的区别

  1. 内存管理:C++中的内存管理是手动的,程序员需要显式地分配和释放内存。C++提供了new和delete关键字来进行动态内存分配和释放。Java中的内存管理是自动的,使用垃圾回收机制来自动管理内存,程序员不需要手动释放内存。
  2. 指针:C++支持指针操作,允许直接访问和修改内存地址。Java中没有指针的概念,所有的数据访问都是通过引用进行的。
  3. 运行环境:C++是一种编译型语言,源代码在编译后被转换为机器码,并直接在操作系统上运行。Java是一种解释型语言,源代码在编译后生成字节码,然后由Java虚拟机(JVM)解释执行。
  4. 平台依赖性:C++代码在不同的平台上需要重新编译,因为它直接与底层系统交互。Java代码是平台无关的,一次编译的字节码可以在任何支持Java虚拟机的平台上运行。
  • C++更适合系统级编程、游戏开发等需要更高的性能和底层控制的场景。
  • Java更适合企业级应用开发、网络编程等需要跨平台和可移植性的场景。

基础知识与坑

程序执行入口

The default program entry function is main, but can be changed in two situations:

  1. use stupid #define xxx main in header file to replace the name which maybe ignored by silly search bar in VSCODE.
  2. use -exxx compile flag

语句末尾的分号

  • 在 C++ 中,是否需要在语句的末尾使用分号(;)取决于上下文。
  • C++ 语法的基本规则是:分号用来标识一条语句的结束,而有些结构并不是严格的语句,因此不需要分号。
  • 声明(变量、类、结构体、枚举、函数原型、类型别名):都需要分号作为结束。
  • 函数定义控制语句(如 if, while, for)、复合语句{}) 不需要分号。
  • 预处理指令(如 #define, #include):不需要分号,因为它们不是 C++ 语法层面的内容。
  • 作用域结束的 } 不需要分号,但声明类或结构体时 } 后要加分号。

为什么类/结构体需要分号

C++ 中的类和结构体定义实际上是一种声明,它们的定义是一种复杂的声明语句,因此必须用分号来结束它们。

总结来说,分号用来结束语句,包括声明、表达式和执行体等,但当你定义一个复合结构(如函数定义、控制语句)时,不需要分号来结束复合结构的定义。

逗号运算符(Comma)

重名变量的优先级

1
2
3
4
5
6
7
8
9
10
int getKthAncestor(int node, int k) {
int node= getKthAncestor(saved[node],--k);
return node;
}
//为什么第二行的node会提前被修改为0,导致传入函数getKthAncestor的saved[node]的node值为0
//如下去掉int,也不会错。因为int node 会初始化node为0
int getKthAncestor(int node, int k) {
node= getKthAncestor(saved[node],--k);
return node;
}

根据C++的作用域规则,内层的局部变量会覆盖外层的同名变量。因此,在第二行的语句中,node引用的是函数参数中的node,而不是你想要的之前定义的node。

为了避免这个问题,你可以修改代码,避免重复定义变量名。例如,可以将第二行的变量名改为newNode或其他不同的名称,以避免与函数参数名冲突。

运算符优先级

运算符性质:

  • 接受的操作数,
  • 优先级,
    • 特殊:逻辑和(&&)先于逻辑或(||)、四则运算先于位运算
    • 位运算优先级低于判断符号,记得写括号。
    • 赋值(=)优先级最低
  • 结合性,
    • 左结合性: 大部分运算(加减乘除)
    • 右结合性:赋值运算符。程序会先计算它右边的表达式的值,然后再计算它左边的表达式的值
  • 返回值
    • 赋值运算符的返回值是赋值后左操作数的引用

变量类型以及Macro constants

https://en.cppreference.com/w/cpp/language/types

https://en.cppreference.com/w/cpp/types/integer

1
2
3
4
5
6
7
8
//返回与平台相关的数值类型的极值
std::numeric_limits<double>::max()
std::numeric_limits<int>::min()

#include<limits.h>
INT_MAX
FLT_MAX (or DBL_MAX )
-FLT_MAX (or -DBL_MAX )

关键词

1
2
3
4
5
extern 
const
constexpr //C++11引入的关键字,用于编译时的常量与常量函数。
volatile //是指每次需要引用某个变量的数据时,都必须从内存原地址读取,而不是编译器优化后寄存器间接读取.(必须写回内存,为了多进程并发而设计的。)
inline

static 关键字

static 作⽤:控制变量的存储⽅式和作用范围(可⻅性)。

  1. 修饰局部变量
    • 存放位置:栈区 -> 静态数据区(data段或者bss段)
    • 生命周期:程序结束才会释放
    • 作用域:还是局部代码块
  2. 修饰函数与全局变量
    • 使其作用范围由全工程文件可见变成了本文件可见

避免

  • 静态变量写到头文件会导致每个引用会有一份;

C++17 以后 局部的const static变量的初始化不是代码运行到才初始化,而是和全局static变量一样,在程序开始执行时初始化。

多文件共用static变量,需要添加 extern 关键字, 去掉static关键字

在 C++ 中,如果你希望 static 变量可以在多个文件中访问,直接写在头文件中是 不正确的,因为每个包含该头文件的源文件都会生成自己的独立 static 变量,导致它们互相独立,无法共享状态。

正确的解决方案

  1. 如果需要共享(全局变量的方式)
    你应该将 encounteredAclops 声明为 extern 变量,并将其定义在一个 .cpp 文件中。

头文件:globals.h

1
2
3
4
5
6
#ifndef GLOBALS_H
#define GLOBALS_H

extern bool encounteredAclops;

#endif // GLOBALS_H

源文件:globals.cpp

1
2
3
#include "globals.h"

bool encounteredAclops = false;

在其他源文件中使用:

1
2
3
4
5
6
7
8
#include "globals.h"

void someFunction() {
if (!encounteredAclops) {
// Do something
encounteredAclops = true;
}
}

通过 extern,所有引用 encounteredAclops 的源文件都会共享同一个变量。


  1. 如果每个文件需要独立的变量

如果每个源文件都需要独立的 encounteredAclops,你可以将 static bool encounteredAclops = false; 放在各自的源文件中,而不需要放在头文件中。这是因为 static 的作用域仅限于当前编译单元。

每个源文件:

1
2
3
4
5
6
7
8
static bool encounteredAclops = false;

void someFunction() {
if (!encounteredAclops) {
// Do something
encounteredAclops = true;
}
}

  1. 如果需要在类中管理(推荐做法)
    可以考虑将 encounteredAclops 作为一个类的静态成员变量来实现共享状态。

头文件:AclopsManager.h

1
2
3
4
5
6
7
8
9
#ifndef ACLOPS_MANAGER_H
#define ACLOPS_MANAGER_H

class AclopsManager {
public:
static bool encounteredAclops;
};

#endif // ACLOPS_MANAGER_H

源文件:AclopsManager.cpp

1
2
3
#include "AclopsManager.h"

bool AclopsManager::encounteredAclops = false;

在其他源文件中使用:

1
2
3
4
5
6
7
8
#include "AclopsManager.h"

void someFunction() {
if (!AclopsManager::encounteredAclops) {
// Do something
AclopsManager::encounteredAclops = true;
}
}

这种方式既可以共享变量,又能保持代码组织清晰。


结论

  • 如果需要共享变量:使用 extern 或类的静态成员变量。
  • 如果需要独立变量:将 static 声明放在各自的源文件中。
  • **不要直接在头文件中定义 static bool encounteredAclops**,否则会导致每个包含头文件的源文件都生成自己的副本,违背初衷。
  1. 修饰类内函数
    • 静态成员函数:使用”static”修饰的成员函数称为静态成员函数。静态成员函数与类的对象无关,可以在没有创建对象的情况下直接通过类名调用。这意味着它们不需要通过类的对象来访问,而是属于整个类的。举例
    • 静态成员函数没有隐式的this指针,因此不能直接访问非静态成员变量和非静态成员函数。静态成员函数可以访问类的静态成员变量和其他静态成员函数。
    • static 成员函数不能被 virtual 修饰, static 成员不属于任何对象或实例,所以加上 virtual没有任何实际意义;
      * 静态成员函数没有 this 指针,虚函数的实现是为每⼀个对象分配⼀个vptr 指针,⽽ vptr 是通过 this 指针调⽤的,所以不能为 virtual;虚函数的调⽤关系,this->vptr->ctable->virtual function。
  2. 修饰类内的变量
    • 存放位置:栈区 -> 静态数据区(data段或者bss段)
    • 生命周期:程序结束才会释放
      • 意味着下一次调用函数时,静态局部变量将保持上一次调用时的值。
    • 由于不再属于某个类对象,可以直接通过类名初始化 int MyClass::staticVariable = 10;

多线程场景,修饰局部变量,会导致多线程共用,建议使用thread_local来避免竞争

static 局部变量在第一次被访问时初始化,且初始化过程是线程不安全的。如果两个线程几乎同时首次访问这个变量,可能会导致初始化竞争,进而引发未定义行为。

static 修饰初始化命令,只会执行一次,无论是否多次经过

问题场景:

static auto thread_core_map = GetCpuAffinityMap(device_id); 即使程序多次调用函数经过这行,但是这行命令也只会执行第一次。但是如果我把static关键词去除,就正常执行多次了。

当在一行代码中使用了 static 关键字时,变量的初始化只会在它第一次被执行时进行,之后即使多次经过这行代码,初始化的代码块也不会被重复执行

在例子中,thread_core_map 是一个静态局部变量,它在第一次经过时会调用 GetCpuAffinityMap(device_id) 函数并保存结果。在后续的函数调用中,即使再次经过这行代码,GetCpuAffinityMap(device_id) 不会被重新调用,因为 thread_core_map 已经被初始化过了。

去除 static 后,thread_core_map 会在每次经过这行代码时重新初始化,也就是每次都会调用 GetCpuAffinityMap(device_id)

解决方法:

如果你需要这行代码每次执行时都重新调用 GetCpuAffinityMap(device_id),那么应该去掉 static 关键字,或根据不同条件进行显式地重新初始化静态变量。

比如,可以这样实现惰性初始化或重置的功能:

1
2
3
4
5
static auto thread_core_map = GetCpuAffinityMap(device_id);
// 根据条件重新初始化
if (/* 条件 */) {
thread_core_map = GetCpuAffinityMap(device_id);
}

这样你就可以在特定条件下让 thread_core_map 被重新赋值。

如果你对静态变量的初始化行为没有问题,但是希望特定场景下重新执行初始化函数,可以根据场景调整条件逻辑。

静态成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

class MyClass {
public:
static void staticFunction() {
std::cout << "This is a static member function." << std::endl;
}
};

int main() {
MyClass::staticFunction(); // 直接通过类名调用静态成员函数
return 0;
}

const 关键字

当const修饰基本数据类型时,可以将其放置在类型说明符的前面或后面,效果是一样的。const关键字用于声明一个常量,即其值在声明后不可修改。

1
2
const int constantValue1 = 10; // const在类型说明符前
int const constantValue2 = 20; // const在类型说明符后

当const关键字位于指针变量或引用变量的左侧时,它用于修饰指针所指向的变量,即指针指向的内容为常量。当const关键字位于指针变量或引用变量的右侧时,它用于修饰指针或引用本身,即指针或引用本身是常量。

  1. 修饰指针指向的变量, 它指向的值不能修改:

    1
    2
    3
    4
    int x = 5;
    const int* ptr = &x; // 指向常量整数的指针
    // *ptr = 10; // 错误:不能通过const指针修改值
    x = 10; // 合法:可以修改变量本身的值
  2. 修饰指针本身 ,它不能再指向别的变量,但指向(变量)的值可以修改。:

    1
    2
    3
    4
    const int y = 10;
    int* const ptr = &y; // 常量指针指向整数
    // ptr = &x; // 错误:不能修改指针本身
    // *ptr = 5; // 合法:可以修改常量变量的值
  3. const int *const p3; //指向整形常量 的 常量指针 。它既不能再指向别的常量,指向的值也不能修改。

explicit

在C++, explicit 是一个关键字,用于修饰单参数构造函数,用于禁止隐式类型转换。

当一个构造函数被声明为 explicit 时,它指示编译器在使用该构造函数进行类型转换时只能使用显式调用,而不允许隐式的类型转换发生。

通过使用 explicit 关键字,可以防止一些意外的类型转换,提高代码的清晰性和安全性。它通常用于防止不必要的类型转换,特别是在单参数构造函数可能引起歧义或产生意外结果的情况下。

preprocessor directive

  • #include_next作用
    在寻找头文件时的头文件搜索优先级里,去除该文件所在的当前目录,主要是为C++头文件的重名问题提供一种解决方案。
    • 正确的用法:代码b.cpp想使用
      自己拓展修改的stdlib.h, 那么在代码的目录下创建stdlib.h,并在该文件里#include_next "stdlib.h" 防止递归引用。

define、 const、 typedef、 inline

  • define:
    • define是一个预处理器指令,用于创建宏定义。它在编译之前对源代码进行简单的文本替换。可以用来定义常量、函数宏和条件编译等。
    • 优势:灵活性上占优,特别是在需要获取文件名、行号控制编译(不同平台编译)时行为的场景
    • 缺点:宏的调试和排错难度相对较高,因为宏的展开发生在编译前,出错时通常不容易直接定位到宏展开的具体代码。相比函数,宏的类型检查不严格,容易导致隐含的错误
    • 例如:#define PI 3.14159,在代码中将PI替换为3.14159。

# 是 字符串化操作符(Stringizing operator)

在 C/C++ 宏中,#字符串化操作符(Stringizing operator),它的作用是将宏参数转换为字符串文字(string literal)。

当在宏中使用 #key 时,key 被转换为一个字符串文字,即在代码中实际变为 "key"(包括引号)。如果不加 #key 将直接作为标记被使用,不会转为字符串。


示例:

以下是一个宏的例子,演示了 # 的作用:

宏定义:

1
2
#define TO_STRING(x) #x
#define CONCAT_AND_PRINT(a, b) printf("Concatenation: %s\n", TO_STRING(a##b));

宏使用:

1
2
3
4
5
6
7
8
9
int main() {
int HelloWorld = 42;

// 使用字符串化操作
printf("%s\n", TO_STRING(HelloWorld)); // 输出: HelloWorld

// 使用标记粘贴操作和字符串化操作
CONCAT_AND_PRINT(Hello, World); // 输出: Concatenation: HelloWorld
}

宏展开与作用:

  1. TO_STRING(HelloWorld)
    TO_STRING 将参数 HelloWorld 转换为字符串文字,展开为:

    1
    printf("%s\n", "HelloWorld");

    输出:HelloWorld

  2. CONCAT_AND_PRINT(Hello, World)

  • a##bHelloWorld 拼接成 HelloWorld
  • TO_STRING(a##b) 将拼接后的标识符 HelloWorld 转换为字符串 "HelloWorld"
    展开为:
    1
    printf("Concatenation: %s\n", "HelloWorld");
    输出:Concatenation: HelloWorld

## 是 C/C++ 宏预处理器的 标记粘贴操作符(Token-pasting operator)

操作符将 valueName 粘贴到 aaa 和 bbb 之间,形成新的标识符。例如,使用 aaa##valueName##bbb 这样的语法是完全有效的。

用于将宏参数和宏内的其他标记连接起来。具体来说,##Value##Initialized 这两个标记会与宏参数(如 valueName)进行拼接,形成新的标识符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define REGISTER_OPTION_CACHE(type, valueName, ...)                 \
static thread_local type valueName##Value; \
static thread_local bool valueName##Initialized = false; \
inline type GetWithCache##valueName() { \
if (!valueName##Initialized) { \
valueName##Value = __VA_ARGS__(); \
valueName##Initialized = true; \
} \
return valueName##Value; \
} \
inline void SetWithCache##valueName(type value) { \
valueName##Value = value; \
valueName##Initialized = true; \
}

宏展开解释

假设你在代码中使用了如下调用:

1
REGISTER_OPTION_CACHE(int, MyValue, 42);

这将会将宏中的 type 替换为 intvalueName 替换为 MyValue,并且 __VA_ARGS__ 代表了传递给宏的可变参数 42

展开后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static thread_local int MyValueValue;
static thread_local bool MyValueInitialized = false;

inline int GetWithCacheMyValue() {
if (!MyValueInitialized) {
MyValueValue = 42; // 使用了 __VA_ARGS__
MyValueInitialized = true;
}
return MyValueValue;
}

inline void SetWithCacheMyValue(int value) {
MyValueValue = value;
MyValueInitialized = true;
}

关键点解释:

  • valueName##Value 被展开为 MyValueValue## 操作符将宏参数 MyValueValue 连接,形成新的标识符 MyValueValue
  • valueName##Initialized 被展开为 MyValueInitialized,同样是将宏参数 MyValueInitialized 连接,形成新的标识符 MyValueInitialized

结果

  • MyValueValue 存储了缓存的值(在这个例子中是 42)。
  • MyValueInitialized 是一个布尔值,用来标记缓存是否已经初始化。
  • GetWithCacheMyValue() 函数首先检查 MyValueInitialized 是否为 true,如果没有被初始化,它会使用 42 来初始化缓存并设置标志。
  • SetWithCacheMyValue(int value) 函数允许你更新缓存的值,并将 MyValueInitialized 设置为 true
  • const:

    • const用于声明一个常量,指示标识符的值在程序执行期间不能被修改。
    • const可以用于变量、函数参数、函数返回类型和成员函数。使用const可以提高代码的可读性和安全性。
    • 例如:const int MAX_VALUE = 100;,声明一个名为MAX_VALUE的常量。
  • typedef:

    • typedef用于为数据类型创建别名。它可以用于为复杂的数据类型提供更简洁的名称,增强代码的可读性和可维护性。
    • typedef创建的别名可以像原始类型一样使用,并且不会引入新的类型,只是为已有类型提供了一个新的名称。
    • 例如:typedef int Age;,为int类型创建了一个别名Age。
  • inline:

    • inline用于声明内联函数,它是一种编译器的建议,用于将函数的定义直接插入到调用处,以避免函数调用的开销。
    • 内联函数通常在函数体较小且频繁调用的情况下使用,可以提高程序的执行效率。
    • inline关键字只是给编译器一个提示,编译器可以选择忽略该提示。在大多数情况下,编译器会自动进行内联优化。
    • 例如:inline int add(int a, int b) { return a + b; },声明了一个内联函数add。
  • define主要用于宏定义,const用于声明常量,typedef用于创建类型别名,inline用于内联函数的声明。

#ifndef & #pragma once

为了避免同一个文件被include多次,C/C++中有两种方式,一种是#ifndef方式,一种是#pragma once方式。在能够支持这两种方式的编译器上,二者并没有太大的区别,但是两者仍然还是有一些细微的区别

new & delete

  • new和delete 相对于 malloc/free 分配和释放堆空间。
    • 额外会执行构造函数和析构函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

class MyClass {
public:
MyClass() {
std::cout << "Constructing MyClass" << std::endl;
}

~MyClass() {
std::cout << "Destructing MyClass" << std::endl;
}
};

int main() {
// 使用new动态分配内存,并调用构造函数
MyClass* obj = new MyClass();

// 执行一些操作...

// 使用delete释放内存,并调用析构函数
delete obj;

return 0;
}

namespace

namespace 会影响 typedef 的作用范围,但不会直接限制 #define 宏的作用范围。

头文件

  • 相互引用,前置声明
  • include头文件其实就是将对应的头文件内容贴在include的位置

A.h, B.h 都需要string.h的头文件,然后B.h 会include A.h,那么我在B.h里是不是可以省略include string.h

不应该省略,

  1. 防止代码变更引发的问题: 如果某天 A.h 中移除了 #include ,而 B.h 依赖 A.h 提供的 #include ,那么 B.h 将会因找不到 std::string 而编译失败。因此,显式包含依赖的头文件可以避免这种隐含依赖引发的问题。
  2. 提高可读性和自包含性: 每个头文件应该尽量做到自包含,意思是每个头文件应该独立地包含所有它所需要的头文件。这样做的好处是,任何其他文件都可以安全地单独包含 B.h,而无需额外关心它依赖于哪些头文件。
  3. 减少隐式依赖: 隐式依赖(依赖另一个头文件帮你包含所需的头文件)可能导致维护性问题。显式 #include 可以让代码更具可预测性和可维护性。

include的位置有什么规则和规律吗,头文件和cpp文件前都可以吗?

在编写代码时,往往A.cpp需要include A.h。那A.cpp需要的头文件,我是写在A.cpp里还是A.h里?

  1. 头文件 (A.h):只包含声明所需的头文件,不包含仅在实现中需要的头文件。
  2. 源文件 (A.cpp):包含所有实现需要的头文件,特别是那些仅在实现部分用到的头文件。此外,A.cpp 应该总是包含 A.h。6.

函数的特殊写法

函数传参

  1. 值传递
  2. 引用传递
  3. 指针传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//值传递
change1(n);
void change1(int n){
n++;
}

//引用传递,操作地址就是实参地址 ,只是相当于实参的一个别名,在符号表里对应是同一个地址。对它的操作就是对实参的操作
change2(n);
void change2(int &n){
n++;
}
//特殊对vector
void change2(vector<int> &n)
//特殊对数组
void change2(int (&n)[1000])

//指针传递,其实是地址的值传递
change3(&n);
void change3(int *n){
*n=*n+1;
}

引用传递和指针传递的区别:

  • 引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
  • 不能有NULL引用,引用必须与合法的存储单元关联(指针则可以是NULL)。
  • 一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。

指针传递和引用传递的使用情景:

  1. 函数内部修改参数并且希望改动影响调用者。
  2. 当一个函数实际需要返回多个值,而只能显式返回一个值时,可以将另外需要返回的变量以指针/引用传递

闭包、匿名函数

闭包是捕获并持有了外部作用域变量的函数。

闭包(Closure)是指在程序中,函数可以捕捉并记住其作用域(环境)中的变量,即使在函数执行完成后,这些变量依然保存在内存中,并能在后续的函数调用中被使用。闭包的一个重要特性是,它不仅保存了函数本身的逻辑,还“闭合”了函数执行时的上下文环境(即该函数所在的作用域)。

闭包通常用于实现函数内部的状态保持、回调函数等场景。在 C++ 中,闭包通过 lambda 表达式 实现,lambda 表达式可以捕获外部变量并在其内部使用。

例子

1
2
3
4
5
6
7
8
auto add = [](int x) {
return [x](int y) {
return x + y;
};
};

auto add5 = add(5);
std::cout << add5(3); // 输出 8

在上面的例子中,add 函数返回了一个闭包,捕获了变量 x 的值。即使 x 在原作用域中不再可用,返回的闭包仍然可以访问并使用 x 的值。

  • 匿名函数是一种没有被绑定标识符的函数
  • lambda 是一种匿名函数
  • lambda 可以表示闭包

匿名函数(lambda)和闭包的关系就如同类和类对象的关系

匿名函数和类的定义都只存在于源码(代码段)中,而闭包和类对象则是在运行时占用内存空间的实体;

传参默认值

虽然理论上可以通过类似void f(bool x = true)来实现默认值。

1
2
3
4
5
6
7
// 函数声明
void bar(int a = 10);

// 函数定义
void bar(int a) {
std::cout << "a: " << a << std::endl;
}

有时(复杂项目)会编译.so不过, 会出现undefied的符号。

导致实际编码如下:

1
2
3
4
5
6
7
8
9
10
11
12
// hpp
void SetDeterministic();
void SetDeterministic(bool isOpapi = true);

//cpp
void SetDeterministic() {
SetDeterministic(true);
}
void SetDeterministic(bool isOpapi)
{
//xxx
}

类型变参模板

1
2
3
4
5
6
7
8
template<typename T>
void swap(T& t1, T& t2)
{
T temp = t2;
t2 = t1;
t1 = temp;
}
swap<int>(a,b);

条件模板类

假设我们有一个模板类 Wrapper,我们希望禁止 VirtualGuardImpl 类型作为模板参数:

1
2
3
4
5
6
7
8
9
10
template <
typename T,
typename U = T,
typename = typename std::enable_if<!std::is_same<U, VirtualGuardImpl>::value>::type>
class Wrapper {
public:
void function() {
// 实现
}
};

在这个例子中,如果用户尝试创建 Wrapper<VirtualGuardImpl>Wrapper<VirtualGuardImpl, VirtualGuardImpl> 的实例,编译器将报错,因为 std::enable_if 的条件不满足。但如果使用其他类型,比如 int 或自定义类型,就可以正常编译。

这段代码是 C++ 中的一个模板函数或模板类模板参数的定义,它使用了模板默认参数、std::enable_if 条件编译技术以及类型萃取(type traits)。下面是对这段代码的详细解释:

  1. 模板参数 U:

    • typename U = T 定义了一个模板类型参数 U,并给它一个默认值 T。这意味着如果在使用模板时没有指定 U 的话,它将默认使用模板参数 T 的值。
  2. std::enable_if:

    • std::enable_if 是一个条件编译技术,它只在给定的布尔表达式为 true 时启用某个模板。
    • 在这个例子中,std::enable_if 后面的布尔表达式是 !std::is_same<U, VirtualGuardImpl>::value。这意味着只有当 U 不等于 VirtualGuardImpl 类型时,这个模板参数才有效。
  3. typename 关键字:

    • typename 关键字用于告诉编译器 std::enable_if 的结果是一个类型。std::enable_if 返回的是一个类型,如果条件为 true,它返回一个空的类型,否则会导致编译错误。
  4. std::is_same:

    • std::is_same<U, VirtualGuardImpl>::value 是一个编译时检查,用于判断 UVirtualGuardImpl 是否是相同的类型。::value 是类型特征 std::is_same 的一个成员,它是一个布尔值,如果类型相同则为 true,否则为 false
  5. 组合解释:

    • 这段代码的意思是:定义一个模板参数 U,默认值为 T,并且这个模板参数只有在 U 不是 VirtualGuardImpl 类型时才有效。
    • 这是一种常见的模板编程技巧,用于约束模板参数的类型,以确保它们符合特定的要求。

个数变参模板

1
2
3
4
5
6
7
8
#include <stdarg.h>
void Error(const char* format, ...)
{
va_list argptr;
va_start(argptr, format);
vfprintf(stderr, format, argptr);
va_end(argptr);
}

VA_LIST 是在C语言中解决变参问题的一组宏,变参问题是指参数的个数不定,可以是传入一个参数也可以是多个;可变参数中的每个参数的类型可以不同,也可以相同;可变参数的每个参数并没有实际的名称与之相对应,用起来是很灵活。

  1. 首先在函数里定义一具VA_LIST型的变量,这个变量是指向参数的指针;
  2. 然后用VA_START宏初始化变量刚定义的VA_LIST变量;
  3. 然后用VA_ARG返回可变的参数,VA_ARG的第二个参数是你要返回的参数的类型(如果函数有多个可变参数的,依次调用VA_ARG获取各个参数);
  4. 最后用VA_END宏结束可变参数的获取。

系统提供了vprintf系列格式化字符串的函数,用于编程人员封装自己的I/O函数。

1
2
3
int vprintf  / vscanf   (const char * format, va_list ap);                  // 从标准输入/输出格式化字符串 
int vfprintf / vfsacanf (FILE * stream, const char * format, va_list ap); // 从文件流
int vsprintf / vsscanf (char * s, const char * format, va_list ap); // 从字符串

返回多个数

使用结构体

1
2
3
4
5
6
7
8
9
struct RowAndCol { int row;int col; };

RowAndCol r(string fn) {
/*...*/
RowAndCol result;
result.row = x;
result.col = y;
return result;
}

左值与右值

在 C++ 中,左值(lvalue)右值(rvalue) 是两个重要的概念,用来描述表达式的值和内存的关系。它们帮助开发者理解变量的生命周期、赋值和对象管理,特别是在现代 C++ 中引入了右值引用后,优化了移动语义和资源管理。

1. 左值(lvalue)

左值(lvalue,locatable value) 是指在内存中有明确地址、可持久存在的对象,可以对其进行赋值操作。通俗地说,左值是能够取地址的值,可以出现在赋值操作符的左边。

特点:

  • 左值具有持久的内存地址。
  • 左值可以取地址(使用 & 运算符)。
  • 左值通常表示已经存在的变量或对象。

示例

1
2
3
4
int x = 10;   // x 是左值
int* p = &x; // 可以取 x 的地址

x = 20; // 可以对左值进行赋值

在这个例子中,x 是一个左值,因为它表示了内存中的某个对象,并且可以通过赋值语句修改它的值。

2. 右值(rvalue)

右值(rvalue,readable value) 是没有明确地址、临时存在的对象,不能对其进行赋值操作。它们通常是字面值常量或表达式的结果。右值只能出现在赋值操作符的右边,表示一个临时对象或数据。

特点:

  • 右值是临时的,通常会在表达式结束时销毁。
  • 右值不能取地址(即不能使用 & 获取右值的地址)。
  • 右值表示表达式的计算结果或临时对象。

示例

1
2
int y = 10;       // 10 是右值
int z = y + 5; // y + 5 是右值表达式

在这个例子中,10y + 5 是右值,因为它们表示计算出的临时数据,并且不能直接对这些值进行赋值操作。

3. 现代 C++ 中的右值引用(rvalue reference)

C++11 引入了 右值引用,即通过 && 符号表示。这使得右值也能通过引用进行操作,特别是在实现移动语义(move semantics)和避免不必要的拷贝时非常有用。右值引用允许我们通过右值管理资源,避免性能上的损失。

示例:右值引用与移动语义

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <vector>

int main() {
std::vector<int> vec1 = {1, 2, 3};
std::vector<int> vec2 = std::move(vec1); // vec1 资源移动到 vec2

std::cout << "vec1 size: " << vec1.size() << std::endl;
std::cout << "vec2 size: " << vec2.size() << std::endl;

return 0;
}

在这个例子中,std::movevec1 变为一个右值引用,使其内部的资源(如动态分配的内存)直接转移给 vec2,避免了拷贝。

4. 区分左值与右值

通常,左值是表示持久存在的对象,可以通过取地址符 & 获取其地址,而右值是临时的、短暂存在的值,不能直接获取其地址。理解这两者对于编写高效的 C++ 代码和使用现代特性(如右值引用和移动语义)非常重要。

常见误区

  • 字面值常量(如 42、’a’)是右值
  • 表达式的结果(如 x + y)通常是右值。
  • 函数返回值若返回的是值,而不是引用,则该返回值是右值。
  • 左值(lvalue) 是可以取地址的值,通常是变量或持久的对象。
  • 右值(rvalue) 是临时值,通常是表达式的结果或字面量。
  • 右值引用(&&)是 C++11 引入的新特性,用来优化资源管理和避免不必要的拷贝操作。

C++11: 花括号初始化列表

使用

在C++98/03中我们只能对普通数组和POD(plain old data,简单来说就是可以用memcpy复制的对象)类型可以使用列表初始化,如下:

1
2
3
4
5
6
7
8
//数组的初始化列表: 
int arr[3] = {1,2,3}
//POD类型的初始化列表:
struct A
{
int x;
int y;
}a = {1,2};

在C++11中初始化列表被适用性被放大,可以作用于任何类型对象的初始化。如下:

1
2
3
4
5
6
7
8
9
X x1 = X{1,2};
X x2 = {1,2}; // 此处的'='可有可⽆
X x3{1,2};
X* p = new X{1,2};

//列表初始化也可以用在函数的返回值上
std::vector<int> func() {
return {};
}

变量类型的适用范围

聚合类型可以进行直接列表初始化

聚合类型包括

  1. 普通数组,如int[5],char[],double[]等
  2. 一个类,且满足以下条件:
    1. 没有用户声明的构造函数
    2. 没有用户提供的构造函数(允许显示预置或弃置的构造函数)
    3. 没有私有或保护的非静态数据成员
    4. 没有基类
    5. 没有虚函数
    6. 没有{}和=直接初始化的非静态数据成员
    7. 没有默认成员初始化器

原理

对于一个聚合类型,使用列表初始化相当于使用std::initializer_list对其中的相同类型T的每个元素分别赋值处理,类似下面示例代码;

1
2
3
4
5
6
7
8
struct CustomVec {
std::vector<int> data;
CustomVec(std::initializer_list<int> list) {
for (auto iter = list.begin(); iter != list.end(); ++iter) {
data.push_back(*iter);
}
}
};

优势

  1. 方便,且基本上可以替代括号初始化
  2. 可以使用初始化列表接受任意长度
  3. 可以防止类型窄化,避免精度丢失的隐式类型转换

参考文献

  1. https://zh.cppreference.com/

  2. https://leetcode-cn.com/problems/path-with-maximum-gold/solution/huang-jin-kuang-gong-by-leetcode-solutio-f9gg/

  3. https://blog.csdn.net/qq_33221533/article/details/82119031

  4. ⼩贺 C++ ⼋股⽂ PDF 的作者,电⼦书的内容整理于公众号「herongwei」

  5. https://blog.csdn.net/hailong0715/article/details/54018002

https://shaojiemike.notion.site/C-11-a94be53ca5a94d34b8c6972339e7538a

6 FPS

卡拉彼丘

  • 信息差:发现分布以及落单对方,灵活跑动隐藏自身,干扰对方准备好时,对方换弹/倒地时补射。
    • 躲避对方多枪线,己方架多枪线,多路线包围
    • 一个位置偷一枪,就换位置。不要再露头。
    • 对枪注意弦化
    • 位置的选择:一要有掩体,二要有安全的退路通道和队友大部队汇合,不要被敌人包夹。
    • 不要急于补人,要观察有没有被敌人包
    • 进阶:时刻预瞄出人点,
      • 弦化靠左墙,预瞄靠左,因为向右出掩体,准星会被向右移动。
      • 学习弹道,反向压枪。
  • 注意不要冲动,以身试陷(除非是突破位)

角色特点

  1. 熊当掩体(带闪光弹,烟雾弹),熊会自动冲锋并结冰
  2. 防守方
    1. 米雪儿:适合压制补枪,技能适合补枪。引诱敌方到背面炮台射程里
  3. 进攻方:
    1. 明:侦察 + 干扰器,风场雷

地图,高空卡墙脚。

  1. 欧拉港口/海湾图:复杂的短距离(掩体之间的距离)小路。适合白墨(带烟雾弹增加自身能力)和熊。白墨攻击走中间,抄底路偷对面的大狙。或者A点上上下下,适合近身跳散弹。
    1. 防守走A
  2. 404基地/巨炮图:白墨可以中路强压。
    1. 防守方 熊,进攻方沙猫无敌B
  3. 88区/古风图,大图远视野,适合大狙,大机枪。还有熊
    1. 禁止白墨。
  4. 风曳镇:大狙和小画家
    1. 防守必选熊(AB滑)和信(传送)
    2. 禁止白墨。

需要进一步的研究学习

暂无

遇到的问题

暂无

开题缘由、总结、反思、吐槽~~

参考文献

上面回答部分来自ChatGPT-3.5,没有进行正确性的交叉校验。

Python

解释性语言

解释型语言没有严格编译汇编过程,由解释器将代码块按需要变运行边翻译给机器执行。因此解释型语言一度存在运行效率底,重复解释的问题。但是通过对解释器的优化!可以提高解释型语言的运行效率。

Python 与大多数解释型语言一样,确实是将源代码编译为一组虚拟机指令,并且 Python 解释器是针对相应的虚拟机实现的。这种中间格式被称为 “字节码”。

Python 字节码

Python 以 .pyc 结尾的 “ 字节码(bytecode)” 文件(二进制文件),一般位于__pycache__ 的子目录中,可以避免每次运行 Python 时去重新解析源代码。

1
python -m py_compile file.py  # 生成单个pyc文件

python程序运行中的 Python 字节码 是什么时候产生的,我修改py文件,其对应的字节码文件会在何时更新

gpt3.5: Python字节码是由Python解释器在执行Python源代码之前生成的中间表示形式。它是将源代码翻译为机器可以理解的指令序列的一种形式。字节码文件的扩展名通常为.pyc(Python 3)或.pyo(Python 2)。

当你运行Python程序时,如果存在对应的字节码文件(.pyc或.pyo),解释器会首先检查字节码文件的时间戳。如果字节码文件的时间戳比源代码文件的时间戳更晚,且没有进行代码的修改,解释器会直接加载和执行字节码,跳过源代码的编译过程。

当你修改了Python源代码文件时,字节码文件不会立即更新。Python解释器会在下一次运行程序时,检测到源代码文件的修改,并重新编译生成新的字节码文件。这样,更新后的字节码文件将反映源代码的更改,并用于后续的执行。

访问和理解 Python 字节码

经过编译的python文件可以提高程序的运行速度,一定程度上也对源代码起到了保护作用。然而如果我们只有编译过的python字节码文件,就给我们审查源码造成了一定的困难,这就引出了python字节码反编译的需求。

如果你想玩转字节码,那么,Python 标准库中的 dis 模块将对你有非常大的帮助;dis 模块为 Python 字节码提供了一个 “反汇编”,它可以让你更容易地得到一个人类可读的版本,以及查找各种字节码指令。

知道如何去访问和阅读 Python 字节码将让你很容易回答为什么某些结构比其它结构运行的更快这样的问题(比如,为什么 {} 比 dict() 快)(尝试对比一下: dis.dis(“{}”) 与 dis.dis(“dict()”) 就会明白)。

pyo优化文件

pyo文件是源代码文件经过优化编译后生成的文件,是pyc文件的优化版本。编译时需要使用-O和-OO选项来生成pyo文件。在Python3.5之后,不再使用.pyo文件名,而是生成文件名类似“test.opt-n.pyc的文件。

1
python -O -m py_compile test.py

Python 虚拟机

CPython 使用一个基于栈的虚拟机。(你可以 “推入” 一个东西到栈 “顶”,或者,从栈 “顶” 上 “弹出” 一个东西来)。

CPython 使用三种类型的栈:

  1. 调用栈(call stack)。这是运行 Python 程序的主要结构。它为每个当前活动的函数调用使用了一个东西 —— “ 帧(frame)”
  2. 在每个帧中,有一个 **计算栈(evaluation stack)**(也称为 数据栈(data stack))。这个栈就是 Python 函数运行的地方,运行的 Python 代码大多数是由推入到这个栈中的东西组成的,操作它们,然后在返回后销毁它们。
  3. 在每个帧中,还有一个**块栈(block stack)**。它被 Python 用于去跟踪某些类型的控制结构:循环、try / except 块、以及 with 块,全部推入到块栈中,当你退出这些控制结构时,块栈被销毁。

C vs Python

运行流程区别

python的传统运行执行模式:录入的源代码转换为字节码,之后字节码在python虚拟机中运行。代码自动被编译,之后再解释成机器码在CPU中执行。

c编译器直接把c源代码编译成机器码。过程比python执行过程少了字节码生成和虚拟机执行字节码过程。所以自然比python快。

深、浅拷贝

Python append() 与深拷贝、浅拷贝

python赋值只是引用,别名

1
2
3
4
5
6
7
8
list.append('Google')   ## 使用 append() 添加元素
alist.append( num ) # 浅拷贝 ,之后修改num 会影响alist内的值

import copy
alist.append( copy.deepcopy( num ) ) # 深拷贝

# delete
del list[2]

for循环迭代的元素 也是 引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
original_list = [1, 2, 3]

for item in original_list:
item *= 2 # 每个元素是不可变的

print(original_list)

original_list = [[1,2,3], [2], [3]]

for item in original_list:
item.append("xxx") # 每个元素是可变的

print(original_list)

# [1, 2, 3]
# [[1, 2, 3, 'xxx'], [2, 'xxx'], [3, 'xxx']]

[函数传参是引用,但是能通过切片来得到类似指针](https

参数的传递
函数声明时的形参,使用时,等同于函数体内的局部变量。由于Python中一切皆为对象。因此,参数传递时直接传递对象的地址,但具体使用分两种类型:

  1. 传递不可变对象的引用(起到其他语言值传递的效果) 数字,字符串,元组,function等
  2. 传递可变对象的引用(起到其他语言引用传递的效果) 字典,列表,集合,自定义的对象等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def fun0(a):
a = [0,0] # a在修改后,指向的地址发生改变,相当于新建了一个值为[0,0]

def fun(a):
a[0] = [1,2]

def fun2(a):
a[:] = [10,20]

b = [3,4]
fun0(b)
print(b)
fun(b)
print(b)
fun2(b)
print(b)

# [3, 4]
# [[1, 2], 4]
# [10, 20]

return 返回值, 可变对象的也是引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def fun1(l):
l.append("0")
return l

def fun2(l):
return l

if __name__=="__main__":
l = [1,2,3,4,5]

rel2 = fun2(l)
print(rel2)
rel1 = fun1(l)
print(rel1)
print(rel2)

# [1, 2, 3, 4, 5]
# [1, 2, 3, 4, 5, '0']
# [1, 2, 3, 4, 5, '0']

逻辑

setup

setup安装包的过程,请看pip package一文。

import

命名空间(namespace)可以基本理解成每个文件是一个,通过import来使用

触发 __init__.py

  • 当你导入一个包时,Python 会执行该包目录下的 __init__.py 文件。如果没有这个文件,Python 会认为这个目录不是一个包,因此 import 语句会失败。
  • __init__.py 负责初始化这个包,可以定义一些包级别的变量、函数或导入包的其他子模块。

行为

  • 每次导入包时,__init__.py 文件只会在第一次导入时被执行一次。如果模块已经被导入到当前的命名空间,再次 import 不会重新执行 __init__.py,除非你强制重新加载(比如用 importlib.reload())。
  • import 的执行会触发模块的初始化,类似于 C++ 中构造函数的概念,但不是在对象级别,而是在模块级别。
1
2
3
4
5
# example/__init__.py
print("Initializing the package")

def hello():
print("Hello from the package")
1
2
3
4
import example
# 输出 "Initializing the package"
example.hello()
# 输出 "Hello from the package"

入口

  • 在Python中,if __name__ == "__main__"这种写法通常出现在模块中,它的作用是控制模块的执行流程。
  • 当一个模块被导入时,Python解释器会自动将这个模块的__name__属性设置为模块名称。但是如果模块是被直接运行的,则__name__属性会被设置为字符串__main__。
  • 所以if name == “main”可以用来区分模块是被导入运行还是被直接运行:
  • 如果模块是被导入的,if语句不会执行。因为模块的__name__不等于__main__。
  • 如果模块是被直接运行的,if语句会执行。因为模块的__name__等于__main__。

清理与释放

程序结束时的清理行为(类似析构函数的操作)

在 Python 中,并没有像 C++ 那样显式的析构函数。模块或对象的清理一般通过以下方式实现:

  • 对象的析构:当一个 Python 对象的引用计数降为零时,Python 会自动调用该对象的 __del__ 方法进行资源清理。这个机制类似于 C++ 的析构函数,但触发时机取决于 Python 的垃圾回收机制。
1
2
3
4
5
6
7
8
9
class MyClass:
def __init__(self):
print("Object created")

def __del__(self):
print("Object destroyed")

obj = MyClass()
# 程序结束时,或者当 obj 的引用计数降为 0 时,触发 __del__()
  • 模块的清理:当程序结束时,Python 会尝试清理已加载的模块。这个过程会调用模块内一些特殊的钩子函数来进行必要的清理工作。虽然 Python 没有直接为模块提供析构函数,但是你可以使用 atexit 模块来注册一个函数,确保在程序结束时执行。

示例:使用 atexit 实现模块级别的清理操作

1
2
3
4
5
6
7
8
9
import atexit

def cleanup():
print("Cleaning up resources before program exit")

# 注册一个清理函数,在程序结束时自动调用
atexit.register(cleanup)

print("Program is running")

输出

1
2
Program is running
Cleaning up resources before program exit
  • atexit 模块允许你注册多个函数,它们会在解释器关闭之前按注册顺序依次执行。
  • 这种机制相当于 C++ 中的全局或静态对象析构函数的功能,确保在程序结束时执行一些清理工作。

模块的生命周期总结

  • 初始化:当模块被导入时,Python 会执行模块的顶层代码,包括 __init__.py 文件。这相当于模块的 “构造” 过程。
  • 对象的析构:在 Python 中,通过垃圾回收机制和 __del__ 方法来管理对象的生命周期。通常情况下,当对象不再被引用时,会自动触发清理。
  • 程序结束时的清理:Python 提供了 atexit 模块来执行程序结束时的资源清理操作。你可以在模块中注册一些函数,确保在程序退出时执行清理任务。

与 C++ 的比较

  • Python 的模块和包机制类似于 C++ 中的构造函数,但它的作用范围是模块级别的,而不是对象级别的。
  • Python 通过垃圾回收和 __del__ 方法来处理对象的清理,而不是像 C++ 中的显式析构函数。
  • Python 提供了 atexit 模块来实现程序级别的清理操作,这类似于 C++ 中全局/静态对象的析构行为,但更加灵活。

语法

装饰器 decorator

@能在最小改变函数的情况下,包装新的功能。^1

1
2
3
4
5
6
7
8
9
10
11
12
def use_logging(func):

def wrapper():
logging.warn("%s is running" % func.__name__)
return func()
return wrapper

@use_logging
def foo():
print("i am foo")

foo()

下划线

单下划线、双下划线、头尾双下划线说明:

  • __foo__: 定义的是特殊方法,一般是系统定义名字 ,类似 init() 之类的。
  • _foo: 以单下划线开头的表示的是 protected 类型的变量,即保护类型只能允许其本身与子类进行访问,不能用于 from module import *
  • __foo: 双下划线的表示的是私有类型(private)的变量, 只能是允许这个类本身进行访问了。

函数传参

解包

  • 解包是指将一个容器(如列表、元组或字典)的内容拆分并分配给多个变量或作为参数传递给函数。
  • Python 提供了简洁的语法来实现这一点,使用 *** 分别解包可迭代对象和字典。

*args 和 **kwargs

在 Python 中,*args**kwargs 是非常强大的工具,用于处理可变数量的参数。它们使得函数可以接收任意数量的位置参数和关键字参数,并将这些参数传递给其他函数或方法。让我们详细解释一下你提供的代码片段:

1
2
def __call__(self, *input, **kwargs):
result = self.forward(*input, **kwargs)

为什么使用 *** 可以传递参数

  1. 收集参数
  • *input:收集所有未命名的位置参数(非关键字参数),并将它们打包成一个元组。
  • **kwargs:收集所有未明确列出的关键字参数,并将它们打包成一个字典。
  1. 解包参数
  • 在调用 self.forward 时,*input 将之前收集的位置参数解包为单独的参数传递给 forward 方法。
  • 同样,**kwargs 将之前收集的关键字参数解包为单独的关键字参数传递给 forward 方法。

具体工作原理

当你调用 __call__ 方法时,你可以传递任意数量的位置参数和关键字参数。例如:

1
2
obj = SomeClass()
obj(1, 2, 3, key1='value1', key2='value2')

在这个例子中:

  • 1, 2, 3 被收集到 *input 中,形成元组 (1, 2, 3)
  • key1='value1', key2='value2' 被收集到 **kwargs 中,形成字典 {'key1': 'value1', 'key2': 'value2'}

解包可迭代对象

函数定义中的 *

  • 位置参数收集

  • 在函数定义中,*args 用于收集所有未命名的位置参数(非关键字参数),并将它们打包成一个元组。

  • 强制关键字参数

  • 如果在参数列表中使用了单独的 *,那么 * 后面的所有参数必须以关键字形式传递。

示例

1
2
3
4
5
def example_function(a, b, *args):
print(f"a: {a}, b: {b}")
print("Additional positional arguments:", args)

example_function(1, 2, 3, 4, 5)

输出:

1
2
a: 1, b: 2
Additional positional arguments: (3, 4, 5)

强制关键字参数

1
2
3
4
5
6
7
8
def another_function(a, b, *, x, y):
print(f"a: {a}, b: {b}, x: {x}, y: {y}")

# 下面的调用会报错,因为 x 和 y 必须是关键字参数
# another_function(1, 2, 3, 4)

# 正确的调用方式
another_function(1, 2, x=3, y=4)

输出:

1
a: 1, b: 2, x: 3, y: 4

函数调用中的 *

1
2
3
4
5
6
def sum_three_numbers(x, y, z):
return x + y + z

numbers = [1, 2, 3]
result = sum_three_numbers(*numbers)
print(result) # 输出:6

解包字典

  • 在函数定义中,** 用于将传入的关键字参数打包成一个字典;
  • 而在函数调用中,** 则用于将字典解包为关键字参数。

函数定义中的 **kwargs

当你在函数定义中使用 **kwargs 时,所有未明确列出的关键字参数都会被收集到一个名为 kwargs 的字典中。

1
2
3
4
5
def example_function(a, b, **kwargs):
print(f"a: {a}, b: {b}")
print("Additional arguments:", kwargs)

example_function(1, 2, x=3, y=4)

输出:

1
2
a: 1, b: 2
Additional arguments: {'x': 3, 'y': 4}

函数调用中的 ** 解包字典

当你在函数调用中使用 ** 时,它会将字典中的键值对解包为关键字参数传递给函数。这意味着字典的键会成为参数名,对应的值会成为参数值。

1
2
3
4
5
def another_function(a, b, x, y):
print(f"a: {a}, b: {b}, x: {x}, y: {y}")

args_dict = {'x': 3, 'y': 4}
another_function(1, 2, **args_dict)

输出:

1
a: 1, b: 2, x: 3, y: 4

在这个例子中,args_dict 是一个字典,包含键 xy 及其对应的值。通过 **args_dict,这些键值对被解包为关键字参数传递给 another_function

DEBUG

段错误

  1. 开启 Python 的调试模式
    通过设置环境变量启用 Python 的调试信息,这有助于捕获异常和详细的堆栈信息。

    1
    export PYTHONMALLOC=debug
  2. 使用 faulthandler 模块
    Python 提供了一个 faulthandler 模块,可以用来捕获段错误并打印堆栈信息。你可以在程序的开头添加以下代码来启用它:

    1
    2
    import faulthandler
    faulthandler.enable()

    这将会在段错误发生时输出堆栈跟踪。

  3. 查看 Python 调试输出
    启动 Python 程序时,通过 faulthandler 打印堆栈信息,或通过 GDB 调试 Python 解释器。如果 Python 解释器发生崩溃,faulthandler 会帮助你定位错误。

doctest

函数的单元测试

打印当前堆栈

traceback.print_stack()

VizTracer时间性能分析

1
2
3
4
5
6
7
8
9
10
from viztracer import VizTracer

tracer = VizTracer(max_stack_depth=2) # 限制记录的调用栈深度为2,常用为 50和120
tracer.start()

# 你的代码
your_function()

tracer.stop()
tracer.save("result.json")

icecream for debug

rich 库是icecream的上位替代

  • rich:功能更全:支持任意对象的详细信息,包括method; 支持log;支持进度条;支持打印堆栈。
  • rich:打印更华丽

pprint 也不错

pprint 是 Python 的 pprint 模块中的一个函数,全称是 pretty-print(漂亮打印)。它用于以更易读的格式打印数据结构,如字典、列表等。

1
2
from pprint import pprint
pprint(obj)
  • 优雅打印对象:函数名,结构体
  • 打印行号和栈(没用输入时
  • 允许嵌套(会将输入传递到输出
  • 允许带颜色ic.format(*args)获得ic打印的文本
  • debug ic.disable()and ic.enable()
  • 允许统一前缀 ic.configureOutput(prefix='Debug | ')
  • 不用每个文件import
1
2
3
4
5
6
7
from icecream import ic
ic(STH)

from icecream import install
install()

ic.configureOutput(prefix='Debug -> ', outputFunction=yellowPrint)

icecream 是实时打印

普通print不是实时的,可能会出现,代码顺序在后面的ic反而打印在print前面。为此需要print(xxx,flush=True)

prefix 打印代码位置和时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import datetime
import inspect
from icecream import ic

def ic_with_timestamp(*args):
# Get the current frame's information
frame = inspect.currentframe().f_back # Get the caller's frame
filename = frame.f_code.co_filename # File where the function is called
lineno = frame.f_lineno # Line number where the function is called

timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

# Format the output to include timestamp, file, and line number
return '\n\n%s %s:%d shaojieLog >| ' % (timestamp, filename, lineno)

# Configure icecream to use this custom output function
ic.configureOutput(prefix=ic_with_timestamp)

# Example usage
ic("This is a test message.")

prefix 添加时间

1
2
3
4
5
import datetime
def ic_with_timestamp(*args):
return '\n\n%s shaojieLog >| ' % datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

ic.configureOutput(prefix=ic_with_timestamp)

打印ic间时间间隔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import datetime
import inspect
from icecream import ic

# Initialize a variable to store the time of the last ic call
last_ic_time = None
initial_ic_time = datetime.datetime.now() # Set the initial time when the script starts

# Define a custom function that prepends the time since the last ic call, file name, and line number
def ic_with_timestamp(*args):
global last_ic_time
current_time = datetime.datetime.now()

# Calculate the time difference if there was a previous ic call
if last_ic_time is not None:
time_diff = current_time - last_ic_time
time_diff_str = f" (+{time_diff.total_seconds():.2f}s)"
else:
time_diff_str = ""

# Calculate the time since the initial call
time_since_initial = current_time - initial_ic_time
time_since_initial_str = f" [Total time: {time_since_initial.total_seconds():.2f}s]"

# Update last_ic_time to the current time
last_ic_time = current_time

return f'\n\n{current_time.strftime("%Y-%m-%d %H:%M:%S")}{time_diff_str}{time_since_initial_str} shaojieLog |> '
ic.configureOutput(prefix=ic_with_timestamp)

torchrun等多进程环境,利用dist.rank==0来保证只有一个打印

1
2
3
4
5
6
7
8
# Disable icecream
ic.disable()

# This message will be hidden
ic("This message will NOT be shown")

# Re-enable icecream
ic.enable()

ic()的输出无法被tee的log文件捕获

这个问题与 icecream 库的 ic() 函数的默认输出机制有关。icecream 默认将输出发送到标准错误(stderr),而 tee 命令的默认行为是只捕获标准输出(stdout)。因此,ic() 的输出不会被 tee 捕获到。

要解决这个问题,你可以采取以下几种方式:

  1. 使用 ic() 输出到标准输出
  2. 你可以配置 icecream 的输出流,使其输出到标准输出,而不是默认的标准错误。这样,tee 就可以捕获 ic() 的输出。
1
2
3
4
from icecream import ic
import sys

ic.configureOutput(outputFunction=sys.stdout.write)

这样,ic() 的输出就会被发送到标准输出,然后可以被 tee 命令捕获到。

  1. tee 捕获标准错误和标准输出

你也可以让 tee 捕获标准错误(stderr)和标准输出(stdout),这样无需修改 icecream 的配置。

在你的命令中,可以使用如下方式:

1
python3.8 setup.py build bdist_wheel 2>&1 | tee compile.log

在这个命令中,2>&1 将标准错误重定向到标准输出,因此 tee 可以同时捕获两者。

  1. 使用 tee 捕获标准错误单独输出
如果你只想捕获标准错误的输出,并将其保存到日志文件,可以使用以下命令:

1
python3.8 setup.py build bdist_wheel 1>&2 | tee compile.log
或将 `stderr` 和 `stdout` 单独重定向:
1
python3.8 setup.py build bdist_wheel 2>compile.log

性能优化 与 可视化

定位 Python 中 setup.py 脚本运行缓慢的 热点,可以通过多种方式进行性能分析,具体步骤取决于你想了解的性能细节。以下是几种常见的方法来定位性能瓶颈。

方法 1: 使用 cProfile 进行性能分析

cProfile 是 Python 标准库中用于进行性能分析的工具。你可以用它来跟踪 setup.py 执行时的函数调用并找到性能瓶颈。

cProfile + snakeviz + gprof2dot

1
./gprof2dot.py -f pstats Diff.status | dot -Tpng -o ./output/Diff.png

1.1 使用 cProfile 分析 setup.py

你可以通过 cProfile 运行 setup.py 并生成分析报告:

1
python -m cProfile -o setup.prof setup.py install

这将运行 setup.py 并将性能分析结果保存到 setup.prof 文件中。

1.2 可视化分析报告

使用 pstats 或者第三方工具 snakeviz 来分析 setup.prof

  1. 使用 pstats 来查看分析结果:

    1
    python -m pstats setup.prof

    然后,你可以在 pstats 交互式界面中输入命令,比如:

    • sort cumtime 按总耗时排序。
    • stats 查看函数调用的分析结果。
  2. 安装 snakeviz 来生成Web图形化报告:

    1
    pip install snakeviz

    运行 snakeviz 来可视化分析结果:

    1
    snakeviz setup.prof # deploy to 127.0.0.1:8080

    这样可以生成一个图形化的界面,显示每个函数的执行时间以及调用关系,让你更直观地看到性能瓶颈。

  3. 使用 gprof2dot 生成调用关系图片:

    安装 gprof2dot 工具:pip install gprof2dot

    使用 gprof2dot 将 cProfile 生成的 output.prof 转换为 .dot 文件:gprof2dot -f pstats output.prof | dot -Tsvg -o output.svg

    这里的 -f pstats 表示输入的格式是 cProfile 生成的 pstats 文件。这个命令会将结果转换为 SVG 格式的火焰图,保存为 output.svg。

    打开生成的 SVG 文件,查看火焰图。

  4. 生成火焰图: flameprof

    1. 正常的火焰图说明了上到下的调用关系,倒置火焰图说明了底层最耗时的元素。
    2. python flameprof.py input.prof > output.svg
  5. 生成火焰图(有详细文件路径): flamegraph

    1. flameprof --format=log requests.prof | xxx_path/flamegraph.pl > requests-flamegraph.svg

方法 3: 使用 line_profiler 进行逐行性能分析

如果你想深入了解 setup.py 的某个函数或一组函数的逐行性能,可以使用 line_profiler 工具来分析代码的逐行执行时间。

3.1 安装 line_profiler

1
pip install line_profiler

3.2 添加装饰器

首先,在 setup.py 中找到你想要分析的函数,添加 @profile 装饰器(在 line_profiler 中的分析模式下使用):

1
2
3
@profile
def some_function():
# Your function code

3.3 运行 line_profiler

你可以使用 kernprof.py 来运行 setup.py 并生成逐行性能报告:

1
kernprof -l -v setup.py install

这将运行 setup.py 并生成一份逐行性能分析报告,显示每一行代码的耗时。

方法 4: 使用 Py-Spy 进行实时性能分析(推荐!!!)

Py-Spy 是一个 Python 的取样分析器,它可以在不修改代码的情况下对 Python 程序进行性能分析,并生成实时的性能报告。

py-spy top — xxx 有时会卡住

4.1 安装 Py-Spy

1
pip install py-spy

4.2 运行 Py-Spysetup.py 进行分析

你可以在执行 setup.py 的同时运行 Py-Spy 进行取样分析:

1
py-spy top -- python setup.py install

这会生成一个实时的报告,类似于 top 命令,显示当前正在运行的 Python 函数以及其消耗的 CPU 时间。

4.3 生成火焰图

如果你希望生成一个更直观的火焰图,可以使用 py-spy 生成火焰图文件:

1
py-spy record -o profile.svg -- python setup.py install

然后你可以打开 profile.svg 文件,查看一个交互式的火焰图,清晰展示函数调用的时间分布。

方法 5: 使用 strace 分析系统调用

如果 setup.py 涉及大量的 I/O 操作(比如读写文件或安装依赖包),可能是这些操作导致了性能瓶颈。你可以使用 strace 来分析 setup.py 的系统调用,找到 I/O 操作的瓶颈。

1
strace -tt -T -o strace.log python setup.py install
  • -tt 选项会显示每个系统调用的时间戳。
  • -T 会显示每个系统调用耗时。
  • -o 将结果输出到 strace.log 文件中。

通过查看 strace.log,你可以找出系统调用中哪些操作耗时过长。


总结

  1. 使用 cProfilePy-Spy 进行函数级别的性能分析,找出执行慢的函数。
  2. 如果需要更细粒度的逐行分析,使用 line_profiler 来分析慢的部分。
  3. 如果怀疑是 I/O 问题,用 strace 来检查系统调用。
  4. 使用 time 在脚本中插入计时代码,快速定位长时间的执行步骤。

这些工具可以帮助你定位和修复 setup.py 运行缓慢的热点。

虚拟环境venv

1
2
3
4
5
6
7
8
python3 -m venv name

#在Windows上,运行:
name\Scripts\activate.bat # poweshell运行activate.ps1
#在Unix或MacOS上,运行:
source name/bin/activate
#(这个脚本是为bash shell编写的。如果你使用 csh 或 fish shell,你应该改用 activate.csh 或 activate.fish 脚本。)
python3 setup.py install

实践

  1. 并行调用shell命令,超时kill
  2. 基于Pipe的自定义多进程进度条

数据快速写入和读取文件

任意变量使用pickle

1
2
3
4
5
# 使用二进制
with open('my_dict.json', 'wb') as f:
pickle.dump(my_dict, f)
with open('my_dict.json', 'rb') as f:
loaded_dict = pickle.load(f)

可以序列化的使用json

1
2
3
4
5
6
7
8
import json
# 将 dict 保存为 JSON 格式
with open('my_dict.json', 'w') as f:
json.dump(my_dict, f)

# 加载 dict
with open('my_dict.json', 'r') as f:
loaded_dict = json.load(f)

多个变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 将多个变量组织成字典或列表
data = {
"scaAvgTime": scaAvgTime,
"var2": var2,
"var3": var3
}

result_file = "result.json"

# 将数据写入JSON文件
with open(result_file, "w") as f:
json.dump(data, f)

# 读取JSON文件
with open(result_file, "r") as f:
data = json.load(f)

# 获取保存的变量值
scaAvgTime = data["scaAvgTime"]
var2 = data["var2"]
var3 = data["var3"]

参考文献

https://zhuanlan.zhihu.com/p/39259061

Scons

Scons

  • SCons is a software construction tool that can be used as an alternative to traditional build systems like Make and CMake.
  • It is a Python-based build tool that provides a convenient and flexible way to define and manage the build process for software projects, including C++ programs.

Scons VS cmake

  1. 基于python语言的构建工具,对开发者来说过度自然,简单,no need to learn domain-specific language like cmake

其余cmake有的, Scons 也有。

  1. cross-paltform,
  2. SCons has built-in support for dynamic dependency analysis, meaning it can automatically detect changes in source files and rebuild only what’s necessary. This can result in faster builds for large projects.

Project structure

  1. Sconstruct python file as compile entry

framework grammar

  1. add option for scons command

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    AddOption('--buildDir', 
    dest='buildDir',
    type='string',
    default="build/",
    # default=False,
    nargs=1,
    action='store', # meaning save the string
    # or action='store', meaning True or false
    metavar='DIR',
    help='Base build directory'
    )
    baseBuildDir = GetOption('buildDir')
  2. add sub scons config file and build result path using variant_dir

    1
    env.SConscript("src/SConscript", variant_dir=buildDir, exports= {'env' : env.Clone()})     
  3. achive debug mode

    using scons debug=1 command.

    1
    2
    3
    4
    env = Environment()
    debug = ARGUMENTS.get("debug", 0)
    if int(debug):
    print "in debug mode"

main construct grammar

  1. Define the Build Environment:
    In the SConstruct file, define the build environment by creating an Environment object. You can specify compiler options, flags, include paths, library paths, and other build settings within this object.

    1
    2
    env = Environment(CXX='g++', CCFLAGS=['-O2', '-Wall'], CPPPATH=['include'], LIBPATH=['lib'])
    libEnv = env.Clone()
  2. Specify Source Files and Targets:
    Define the source files for your C++ program and specify the target(s) you want to build using the Program() function.

    1
    2
    3
    4
    source_files = ['main.cpp', 'util.cpp', 'other.cpp']
    # or select the src files
    Object('hello.cpp')
    program = env.Program(target='my_program', source=source_files)

    In this example, main.cpp, util.cpp, and other.cpp are the source files, and my_program is the name of the target executable.

  3. static or dynamic lib

1
2
3
4
5
6
# static
Library("t", Glob("src/*.cpp"))
# dynamic
source = Glob("src/*.cpp")
SharedLibrary("t", source)
Program(["hello.cpp"], LIBS=["t"], LIBPATH=".")
  1. execute command during compilation
  • this is usually to print info
  • The command is executed when any of the specified dependencies (allSrcs, ".git/index", or "SConstruct") change.
1
2
3
4
5
6
7
8
9
10
env.Command(
target='bar.out',
source='bar.in',
action=["rm -f $TARGET", "$BAR_BUILD < $SOURCES > $TARGET"],
ENV={'PATH': '/usr/local/bin/'},
)
env.Command(
versionFile,
allSrcs + [".git/index" "SConstruct"],
'printf "#define ZSIM_BUILDDATE \\"`date "+%Y-%m-%d %T"`\\"\\n#define ZSIM_BUILDVERSION \\"`python misc/getver.py`\\"" >>' + versionFile)

Command

1
2
scons -c  # Clean
scons debug=1 # Rebuild using `SConstruct` file in debug mode

scons-project analysis

TODO: multipim how to add a singel head file during compilation process.

需要进一步的研究学习

暂无

遇到的问题

暂无

开题缘由、总结、反思、吐槽~~

参考文献

上面回答部分来自ChatGPT-3.5,没有进行正确性的交叉校验。

https://scons.org/doc/production/PDF/scons-man.pdf