将模块代码量精简为2%的实践
说明
本文通过目录和代码两个层面分析某产品xDsl驱动模块代码,将其精简为原始代码量的2%。
一 完整代码
某产品xDsl驱动模块目录结构如下所示。其中,二级目录Lxx1通常为芯片厂家代码,Lxx2为自定义适配代码。
├─L010
│ ├─include
│ └─source
├─L020
│ ├─include
│ ├─source
…… ……
├─L200
│ ├─L210
…… ……
│ ├─L260
│ │ ├─L261
│ │ │ ├─include
│ │ │ └─source
│ │ └─L262
│ │ ├─include
│ │ └─source
…… ……
│ └─SELT_DELT
│ ├─include
│ └─source
该产品xDsl模块存在大量已废弃的目录,而在用的目录中也存在大量无用的代码。
二 精简代码
此处“精简”意指直接剔除无用代码,而非借助重构等手段来削减代码量。精简主要分为两个步骤:1. 分析makefile文件,清理不予编译的目录;2. 分析预编译宏,删除当前在用目录中无用的代码。
2.1 清理废弃目录
lnxmkdep.ini文件中定义各编译目录(待编译代码所在目录),xDSL驱动目前用于编译的代码目录如下:
1 [DSLADDR] 2 USE=YES 3 PATH=../../../xDSL/L030 4 COPT_EXTRA= 5 DEPEND_MODULES= all 6 7 8 [L261] 9 USE=YES 10 PATH=../../../xDSL//L200/L260/L261 11 COPT_EXTRA= 12 DEPEND_MODULES= all 13 14 [L262] 15 USE=YES 16 PATH=../../../xDSL//L200/L260/L262 17 COPT_EXTRA= 18 DEPEND_MODULES= all 19 20 [L271] 21 USE=YES 22 PATH=../../../xDSL//L200/L270/L271 23 COPT_EXTRA= 24 DEPEND_MODULES= all 25 26 [L272] 27 USE=YES 28 PATH=../../../xDSL//L200/L270/L272 29 COPT_EXTRA= 30 DEPEND_MODULES= all 31 32 [L290] 33 USE=YES 34 PATH=../../../xDSL//L200/L290 35 COPT_EXTRA= 36 DEPEND_MODULES= all 37 COPT_EXTRA= 38 DEPEND_MODULES= all 39 40 [SELTDELT] 41 USE=YES 42 PATH=../../../xDSL/L200/SELT_DELT 43 COPT_EXTRA=-Os 44 DEPEND_MODULES= all
结合头文件包含情况,可知编译需要L010、L030、L200(L210、L230、L260、L270、L290、SELT_DELT)目录。进一步分析得知,仅用到L210和L230目录的若干头文件,而L260目录存在同名头文件且版本更新,故可替代前两个目录。同时,L270目录用于提供30A功能,而当前产品不需要支持该功能,故可删除。
此时,在用的目录已精简为L010、L030、L200(L260、L290、SELT_DELT)。
2.2 删除无用代码
xDsl驱动代码中包含对于其他各种单板和功能的支持,而多数单板已不再使用,某些功能也并未使用。这些支持在代码中主要通过预处理宏(即#if、#ifdef、#ifndef、#elif等含"#+if"关键字的条件编译语句,统称为#if语句)来控制,例如fsap_prj.h文件定义功能宏(INSTALL_MAP_BONDING等),config.mak文件定义编译宏(_INSTALL_VSLC等),其他宏定义则分散于xDsl目录下各文件中。
删除无用代码,即寻找哪些控制编译的宏未定义,并将其控制的代码删除。但鉴于宏定义的分散性,人工查找和删除条件编译分支显然不现实,必须借助自动化工具。
通过工具剔除未使用的条件编译分支,其原理如下:
1. 在待处理代码(*.c、*.h)中#if语句句首插入gcc扩展的预编译头#warning。
2. 编译待处理代码获取gcc编译输出并进行分析。
3. 编译结果中”#warning”警告所对应的#if语句为TRUE,即所控制的代码段正在使用,应予保留;反之可删除。
开源工具stripcc可较好地完成上述工作。在小工程上试用效果符合期望,但应用到本产品时似乎出现死锁,无法正常工作。该问题在研读和调试其源代码后仍未解决。
以下将基于相同工作原理,借助Python脚本处理,分析预编译宏,标记在用的#if代码段。处理后的源代码示例如下(剔除/*TRUE*/ 标记后即为原始代码):
1 #define BCM_BONDING_ENABLED 2 #define BCM_ENABLED 3 4 /*TRUE*/ #ifdef BCM_BONDING_ENABLED 5 CodeLine1; 6 #endif 7 8 #ifdef BCM_DISABLED 9 CodeLine2; 10 /*TRUE*/ #elif defined BCM_ENABLED 11 CodeLine3; 12 #else 13 #error Defination of BCM_DISABLED or BCM_ENABLED is Required! 14 #endif 15 16 #ifdef BCM_TEST 17 CodeLine4; 18 /*TRUE*/ #else 19 CodeLine5; 20 #endif
其中,若#if句前出现:
- 一个/*TRUE*/:表示该#if句为逻辑真;
- 多个/*TRUE*/:多出现于被广泛引用的头文件内,每次引用对为真的#if处增加一个/*TRUE*/;
- 没有/*TRUE*/:表示该#if句为逻辑假。
注意,若某文件“期望”出现/*TRUE*/ 标记(如#ifndef <头文件宏>)但未出现,则该文件很可能并未编译——可用于甄别无用的文件。
【脚本示例】文件布局如下:
其中,AddWarnsEx.py对源代码添加预编译头#warning。
1 #!/usr/bin/python 2 # -*- coding: utf-8 -*- 3 4 import os, re 5 6 7 CodeDirName = r"E:\ValidMacroExample" 8 9 def AddWarnToFile(strPath): 10 OrigFd = open(strPath) 11 BackFd = open(strPath+"b", 'w+') 12 13 OrigLineNo = 0; 14 while(1): 15 CurCodeLine = OrigFd.readline() 16 OrigLineNo = OrigLineNo + 1 17 if(len(CurCodeLine) == 0): 18 break; 19 20 BackFd.write(CurCodeLine) 21 MacthRes = re.compile(".*#\s*(el|if).*").match(CurCodeLine) 22 if MacthRes != None: 23 InsertCodeLine = "#warning Reach code " + '<File:'+strPath +'>'+ '<Line:'+str(OrigLineNo)+'>' + "\n"; 24 BackFd.write(InsertCodeLine) 25 26 OrigFd.close() 27 BackFd.close() 28 return 29 30 31 def SwapFileStatus(strPath): 32 os.rename(strPath, strPath+'t') 33 os.rename(strPath+'b', strPath) 34 os.rename(strPath+'t', strPath+'b') 35 return 36 37 38 def DirTravel(DirPath): 39 40 #遍历目录中的文件 41 if os.path.isdir(DirPath) == True: 42 FileList = os.listdir(DirPath) 43 else: 44 FileList = [os.path.basename(DirPath)] 45 46 if FileList != []: 47 for File in FileList: 48 #检查目录名或文件名 49 if os.path.isdir(DirPath) == True: 50 FilePath = DirPath + os.sep + File 51 else: 52 FilePath = DirPath 53 54 #文件类型为目录,递归 55 if os.path.isdir(FilePath) == True: 56 DirTravel(FilePath) 57 continue 58 59 #识别C文件和H文件 60 SplitList = File.split('.') 61 #忽略无后缀名的文件 62 if len(SplitList) < 2: 63 continue 64 FileType = SplitList[-1] 65 if FileType == 'c' or FileType == 'h': 66 AddWarnToFile(FilePath) 67 SwapFileStatus(FilePath) 68 return 69 70 71 DirTravel(CodeDirName)
ChkMacrosEx.py分析编译结果并标记为真的#if语句。
1 #!/usr/bin/python 2 # -*- coding: utf-8 -*- 3 4 5 import os, re 6 7 CodeDirName = r"E:\ValidMacroExample" 8 WarnFileName = CodeDirName + r"\Warns.txt" 9 10 11 def RestoreFileStatus(strPath): 12 os.remove(strPath) 13 os.rename(strPath+'b', strPath) 14 return 15 16 17 def ValidMacroInFile(): 18 Fd = open(WarnFileName, 'r') 19 20 while(1): 21 CurCodeLine = Fd.readline() 22 if(len(CurCodeLine) == 0): 23 break; 24 25 MacthRes = re.compile(".*#warning.*<File:(.*)><Line:(\d+)>").match(CurCodeLine) 26 if MacthRes != None: 27 #根据编译警告信息打开相应的源文件(MacthRes.group(1)),修改相应行(MacthRes.group(2)) 28 #全文读入,修改一行,全文写入。同一文件内多行#warning时,效率较低 29 SrcFd = open(MacthRes.group(1), 'r') 30 FileLines = SrcFd.readlines() 31 ModLineNo = int(MacthRes.group(2))-1 32 FileLines[ModLineNo] = "/*TRUE*/ " + FileLines[ModLineNo] 33 SrcFd.close() 34 35 SrcFd = open(MacthRes.group(1), 'w') 36 SrcFd.writelines(FileLines) 37 SrcFd.close() 38 39 Fd.close() 40 return 41 42 43 def RemoveBackFile(DirPath): 44 45 #遍历目录中的文件 46 if os.path.isdir(DirPath) == True: 47 FileList = os.listdir(DirPath) 48 else: 49 FileList = [os.path.basename(DirPath)] 50 51 if FileList != []: 52 for File in FileList: 53 #检查目录名或文件名 54 if os.path.isdir(DirPath) == True: 55 FilePath = DirPath + os.sep + File 56 else: 57 FilePath = DirPath 58 59 #文件类型为目录,递归 60 if os.path.isdir(FilePath) == True: 61 RemoveBackFile(FilePath) 62 continue 63 64 #识别C文件和H文件 65 SplitList = File.split('.') 66 #忽略无后缀名的文件 67 if len(SplitList) < 2: 68 continue 69 FileType = SplitList[-1] 70 if FileType == 'c' or FileType == 'h': 71 RestoreFileStatus(FilePath) 72 #os.remove(FilePath) 73 return 74 75 76 RemoveBackFile(CodeDirName) 77 ValidMacroInFile()
Macro.c等为待处理的C源文件。
1 //Macro.c(dir1) 2 #define BCM_BONDING_ENABLED 3 #define BCM_ENABLED 4 5 #ifdef BCM_BONDING_ENABLED 6 CodeLine1; 7 #endif 8 9 #ifdef BCM_DISABLED 10 CodeLine2; 11 #elif defined BCM_ENABLED 12 CodeLine3; 13 #else 14 #error Defination of BCM_DISABLED or BCM_ENABLED is Required! 15 #endif 16 17 #ifdef BCM_TEST 18 CodeLine4; 19 #else 20 CodeLine5; 21 #endif 22 23 24 //Macro1.c(dir2) 25 #define BCM_BONDING_ENABLED 26 #define BCM_ENABLED 27 28 #ifdef BCM_BONDING_ENABLED 29 CodeLine4; 30 #endif 31 32 #ifdef BCM_ENABLED 33 CodeLine8; 34 #endif 35 36 37 //Macro2.c(dir2) 38 #define BCM_ENABLED 39 40 #ifdef BCM_VECTOR_ENABLED 41 CodeLine4; 42 #endif 43 44 #ifdef BCM_ENABLED 45 CodeLine8; 46 #endif
Warns.txt为编译结果(暂以模拟内容代替)。
1 #warning Reach code <File:E:\ValidMacroExample\dir1\Macro.c><Line:4> 2 #warning Reach code <File:E:\ValidMacroExample\dir1\Macro.c><Line:10> 3 #warning Reach code <File:E:\ValidMacroExample\dir1\Macro.c><Line:18> 4 #warning Reach code <File:E:\ValidMacroExample\dir2\Macro1.c><Line:4> 5 #warning Reach code <File:E:\ValidMacroExample\dir2\Macro1.c><Line:8> 6 #warning Reach code <File:E:\ValidMacroExample\dir2\Macro2.c><Line:7>
根据实际情况调整代码路径(当前为E:\ValidMacroExample)后,按如下步骤运行:
1. 执行AddWarnsEx.py,生成添加#warning后的代码文件f.c(h)及其备份f.cb(hb)。
2. 编译处理后的代码文件f.c(h),将编译结果重定向到Warns.txt内。
3. 执行ChkMacrosEx.py,生成添加/*TRUE*/的代码文件,并自动删除备份文件。
将Python脚本内待处理代码路径修改为xDsl模块路径后,即可用于实际工程代码的精简。经过处理的实际代码片段截图如下:
更进一步,可分析处理后的/*TRUE*/标记,自动删除未编译的代码段,但需要严密的语法分析。此外,目前的脚本实现未考虑执行效率。因时间精力有限,暂时不予改进。
三 效果评价
清理目录和代码后,比较完整代码(Full)和精简代码(Lite)的规模如下:
版本 |
代码量(行) |
系数 |
Full |
9,024,746 |
1 |
Lite |
221,964 |
0.0246 |
可见,Lite代码行数约为Full代码的2%(考虑到BCM芯片SDK后续可能更新,为便于同步相应代码未做精简)。编译后经验证,可正常配置和查询。