构建与GNU Make的常见问题及算术实现
在软件开发过程中,构建系统的效率和功能对于项目的顺利推进至关重要。本文将探讨一些构建相关的常见问题,以及如何利用GNU Make实现算术功能,甚至构建一个简单的计算器。
处理器数量与构建加速
在小型构建任务中,处理器数量对构建速度的提升存在一定的规律。根据Amdahl定律,当处理器数量达到约8个时,最大加速比会趋于平稳。实际情况中,由于构建任务只有13个可能的作业,这个平稳点会受到进一步限制。
以下是不同处理器数量对应的最大加速比表格:
| 处理器数量 | 最大加速比 |
| — | — |
| 10 | 2.46x |
| 11 | 2.50x |
| 12 | 2.53x |
从构建结构来看,最多使用8个处理器。因为有5个作业(t1、t2、t4、t6和t7)可以无依赖地并行运行,另外还有3个小作业链(t3、t5和t8;t9和t10;t11和t12),每个链每次使用一个处理器。而构建任务t可以复用这8个处理器中的一个,因为此时它们都处于空闲状态。
在实际应用中,像C和C++这类有链接步骤的语言,Amdahl定律对构建时间有显著影响。通常,所有目标文件在链接步骤之前构建完成,然后需要运行一个单独(通常很大)的链接进程。这个链接进程通常无法并行化,成为构建并行化的限制因素。
使
$(wildcard)
函数递归
GNU Make内置的
$(wildcard)
函数不是递归的,它只能在单个目录中搜索文件。不过,可以在
$(wildcard)
中使用多个通配符模式来查找子目录中的文件,例如
$(wildcard */*.c)
可以找到当前目录所有子目录中的
.c
文件。但如果需要搜索任意目录树,就没有内置的方法了。
幸运的是,可以很容易地创建一个递归版本的
$(wildcard)
函数:
rwildcard=$(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst *,%,$2),$d))
这个
rwildcard
函数接受两个参数:第一个是搜索起始目录(可以留空以从当前目录开始),第二个是每个目录中要查找文件的通配符模式。
以下是使用示例:
- 查找当前目录(包括子目录)中的所有
.c
文件:
$(call rwildcard,,*.c)
-
查找
/tmp目录中的所有.c文件:
$(call rwildcard,/tmp/,*.c)
rwildcard
函数还支持多个模式,例如:
$(call rwildcard,/src/,*.c *.h)
这将查找
/src/
目录下的所有
.c
和
.h
文件。
确定当前Makefile的位置
在实际开发中,有时需要知道当前正在解析的Makefile的名称和路径。虽然GNU Make没有内置的快速获取方法,但可以使用
MAKEFILE_LIST
变量来实现。
MAKEFILE_LIST
是当前已加载或包含的Makefile列表。每次加载或包含一个Makefile时,其路径和名称会追加到
MAKEFILE_LIST
中。该变量中的路径和名称相对于当前工作目录(GNU Make启动的目录或使用
-C
或
--directory
选项指定的目录),可以通过
CURDIR
变量访问当前目录。
可以定义一个GNU Make函数
where-am-i
来返回当前Makefile:
where-am-i = $(CURDIR)/$(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))
要查找当前Makefile的完整路径,需要在Makefile的顶部添加以下代码:
THIS_MAKEFILE := $(call where-am-i)
这行代码必须放在顶部,因为Makefile中的任何
include
语句都会改变
MAKEFILE_LIST
的值,所以要在这之前获取当前Makefile的位置。
以下是一个示例Makefile,使用
where-am-i
函数并包含其他Makefile:
where-am-i = $(CURDIR)/$(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))
include foo/makefile
foo/makefile
的内容如下:
THIS_MAKEFILE := $(call where-am-i)
$(warning $(THIS_MAKEFILE))
include foo/bar/makefile
foo/bar/makefile
的内容如下:
THIS_MAKEFILE := $(call where-am-i)
$(warning $(THIS_MAKEFILE))
将这三个Makefile放在
/tmp
目录(及其子目录)中并运行GNU Make,输出如下:
foo/makefile:2: /tmp/foo/makefile
foo/bar/makefile:2: /tmp/foo/bar/makefile
GNU Make中的算术实现
GNU Make本身没有内置的算术功能,但可以使用其内置的列表和字符串操作函数来创建整数的加、减、乘、除以及比较函数。以下是具体的实现步骤和代码示例。
数字表示
为了实现算术功能,首先需要一种数字表示方法。这里使用包含相应数量元素的列表来表示数字,例如用
x x x x x
表示数字5。
可以使用
$(words)
函数将这种内部形式(全是
x
)转换为人类可读的形式:
five := x x x x x
all: ; @echo $(words $(five))
还可以创建一个用户定义的函数
decode
来进行转换:
decode = $(words $1)
使用示例:
five := x x x x x
all: ; @echo $(call decode,$(five))
加法和减法
有了数字表示方法后,就可以定义加法、递增(加1)和递减(减1)函数:
plus = $1 $2
increment = x $1
decrement = $(wordlist 2,$(words $1),$1)
plus
函数将两个参数组合成一个列表,通过拼接实现加法;
increment
函数在参数前添加一个
x
;
decrement
函数从索引2开始获取整个
x
字符串,从而去掉第一个
x
。
以下代码将输出11:
two := x x
three := x x x
four := x x x x
five := x x x x x
six := x x x x x x
all: ; @echo $(call decode,$(call plus,$(five),$(six)))
还可以创建一个简单的函数
double
来将参数翻倍:
double = $1 $1
在实现减法之前,先实现
max
和
min
函数:
max = $(subst xx,x,$(join $1,$2))
min = $(subst xx,x,$(filter xx,$(join $1,$2)))
max
函数使用
$(join)
和
$(subst)
函数,
$(join)
将两个列表连接起来,
$(subst)
将匹配的元素替换为指定值。例如,计算
$(call max,$(five),$(six))
的过程如下:
$(call max,$(five),$(six))
-> $(call max,x x x x x,x x x x x x)
-> $(subst xx,x,$(join x x x x x,x x x x x x))
-> $(subst xx,x,xx xx xx xx xx x)
-> x x x x x x
min
函数使用类似的方法,但只保留
xx
元素:
$(call min,$(five),$(six))
-> $(call min,x x x x x,x x x x x x)
-> $(subst xx,x,$(filter xx,$(join x x x x x,x x x x x x)))
-> $(subst xx,x,$(filter xx,xx xx xx xx xx x))
-> $(subst xx,x,xx xx xx xx xx)
-> x x x x x
减法函数的实现如下:
subtract = $(if $(call gte,$1,$2), \
$(filter-out xx,$(join $1,$2)), \
$(warning Subtraction underflow))
这里使用
gte
函数(大于或等于)来检查第一个参数是否大于或等于第二个参数,以避免减法下溢。
gte
函数由
gt
(大于)和
eq
(等于)函数实现:
gt = $(filter-out $(words $2),$(words $(call max,$1,$2)))
eq = $(filter $(words $1),$(words $2))
gte = $(call gt,$1,$2)$(call eq,$1,$2)
以下是
eq
函数的示例:
$(call eq,$(five),$(five))
-> $(call eq,x x x x x,x x x x x)
-> $(filter $(words x x x x x),$(words x x x x x))
-> $(filter 5,5)
-> 5
当两个参数相等时,
eq
函数返回一个非空字符串,表示为真。
还可以定义其他比较运算符,如
ne
(不等于)、
lt
(小于)和
lte
(小于或等于):
lt = $(filter-out $(words $1),$(words $(call max,$1,$2)))
ne = $(filter-out $(words $1),$(words $2))
lte = $(call lt,$1,$2)$(call eq,$1,$2)
乘法和除法
乘法函数使用
$(foreach)
函数实现:
multiply = $(foreach a,$1,$2)
例如:
$(call multiply,$(two),$(three))
-> $(call multiply,x x,x x x)
-> $(foreach a,x x,x x x)
-> x x x x x x
除法函数是最复杂的,需要递归实现:
divide = $(if $(call gte,$1,$2), \
x $(call divide,$(call subtract,$1,$2),$2),)
如果第一个参数小于第二个参数,除法返回0。例如:
$(call divide,$(three),$(two))
-> $(call divide,x x x,x x)
-> $(if $(call gte,x x x,x x),
x $(call divide,$(call subtract,x x x,x x),x x),)
-> x $(call divide,$(call subtract,x x x,x x),x x)
-> x $(call divide,x,x x)
-> x $(if $(call gte,x,x x),
x $(call divide,$(call subtract,x,x x),x x),)
-> x
对于除以2的特殊情况,可以定义
halve
函数来避免递归:
halve = $(subst xx,x, \
$(filter-out xy x y, \
$(join $1,$(foreach a,$1,y x))))
最后,需要一个
encode
函数将用户输入的数字转换为
x
字符串:
16 := x x x x x x x x x x x x x x x x
input_int := $(foreach a,$(16), \
$(foreach b,$(16), \
$(foreach c,$(16),$(16)))))
encode = $(wordlist 1,$1,$(input_int))
这里目前只能输入最大为65536的数字,可以通过改变
input_int
中
x
的数量来扩展。
使用算术库构建计算器
为了展示这个算术库的功能,可以实现一个完全用GNU Make函数编写的逆波兰表达式计算器。
stack :=
push = $(eval stack := $$1 $(stack))
pop = $(word 1,$(stack))$(eval stack := $(wordlist 2,$(words $(stack)),$(stack)))
pope = $(call encode,$(call pop))
pushd = $(call push,$(call decode,$1))
comma := ,
calculate = $(foreach t,$(subst $(comma), ,$1),$(call handle,$t))$(stack)
seq = $(filter $1,$2)
handle = $(call pushd, \
$(if $(call seq,+,$1), \
$(call plus,$(call pope),$(call pope)), \
$(if $(call seq,-,$1), \
$(call subtract,$(call pope),$(call pope)), \
$(if $(call seq,*,$1), \
$(call multiply,$(call pope),$(call pope)), \
$(if $(call seq,/,$1), \
$(call divide,$(call pope),$(call pope)), \
$(call encode,$1))))))
.PHONY: calc
calc: ; @echo $(call calculate,$(calc))
运算符和数字通过
calc
变量传递给GNU Make,用逗号分隔。例如:
$ make calc="3,1,-,3,21,5,*,+,/"
54
以下是完整的注释版Makefile:
# input_int consists of 65536 x's built from the 16 x's in 16
16 := x x x x x x x x x x x x x x x x
input_int := $(foreach a,$(16),$(foreach b,$(16),$(foreach c,$(16),$(16)))))
# decode turns a number in x's representation into an integer for human
# consumption
decode = $(words $1)
# encode takes an integer and returns the appropriate x's
# representation of the number by chopping $1 x's from the start of
# input_int
encode = $(wordlist 1,$1,$(input_int))
# plus adds its two arguments, subtract subtracts its second argument
# from its first if and only if this would not result in a negative result
plus = $1 $2
subtract = $(if $(call gte,$1,$2), \
$(filter-out xx,$(join $1,$2)), \
$(warning Subtraction underflow))
# multiply multiplies its two arguments and divide divides its first
# argument by its second
multiply = $(foreach a,$1,$2)
divide = $(if $(call gte,$1,$2),x $(call divide,$(call subtract,$1,$2),$2),)
# max returns the maximum of its arguments and min the minimum
max = $(subst xx,x,$(join $1,$2))
min = $(subst xx,x,$(filter xx,$(join $1,$2)))
# The following operators return a non-empty string if their result is true:
#
# gt First argument is greater than second argument
# gte First argument is greater than or equal to second argument
# lt First argument is less than second argument
# lte First argument is less than or equal to second argument
# eq First argument is numerically equal to the second argument
# ne First argument is not numerically equal to the second argument
gt = $(filter-out $(words $2),$(words $(call max,$1,$2)))
lt = $(filter-out $(words $1),$(words $(call max,$1,$2)))
eq = $(filter $(words $1),$(words $2))
ne = $(filter-out $(words $1),$(words $2))
gte = $(call gt,$1,$2)$(call eq,$1,$2)
lte = $(call lt,$1,$2)$(call eq,$1,$2)
# increment adds 1 to its argument, decrement subtracts 1. Note that
# decrement does not range check and hence will not underflow, but
# will incorrectly say that 0 - 1 = 0
increment = $1 x
decrement = $(wordlist 2,$(words $1),$1)
# double doubles its argument, and halve halves it
double = $1 $1
halve = $(subst xx,x,$(filter-out xy x y,$(join $1,$(foreach a,$1,y x))))
# This code implements a Reverse Polish Notation calculator by
# transforming a comma-separated list of operators (+ - * /) and
# numbers stored in the calc variable into the appropriate calls to
# the arithmetic functions defined in this makefile.
# This is the current stack of numbers entered into the calculator. The push
# function puts an item onto the top of the stack (the start of the list), and
# pop removes the top item.
stack :=
push = $(eval stack := $$1 $(stack))
pop = $(word 1,$(stack))$(eval stack := $(wordlist 2,$(words $(stack)),$(stack)))
# pope pops a number off the stack and encodes it
# and pushd pushes a number onto the stack after decoding
pope = $(call encode,$(call pop))
pushd = $(call push,$(call decode,$1))
# calculate runs through the input numbers and operations and either
# pushes a number on the stack or pops two numbers off and does a
# calculation followed by pushing the result back. When calculate is
# finished, there will be one item on the stack, which is the result.
comma := ,
calculate=$(foreach t,$(subst $(comma), ,$1),$(call handle,$t))$(stack)
# seq is a string equality operator that returns true (a non-empty
# string) if the two strings are equal
seq = $(filter $1,$2)
# handle is used by calculate to handle a single token. If it's an
# operator, the appropriate operator function is called; if it's a
# number, it is pushed.
handle = $(call pushd, \
$(if $(call seq,+,$1), \
$(call plus,$(call pope),$(call pope)), \
$(if $(call seq,-,$1), \
通过以上内容,我们了解了构建过程中处理器数量与加速比的关系,掌握了
$(wildcard)
函数的递归使用和确定当前Makefile位置的方法,还实现了GNU Make中的算术功能和一个简单的计算器。这些技巧和方法在实际开发中可以帮助我们提高构建效率和解决一些复杂的问题。
构建与GNU Make的常见问题及算术实现
逆波兰表达式计算器的工作流程
为了更好地理解逆波兰表达式计算器的实现,下面详细介绍其工作流程。
graph TD
A[输入表达式] --> B[分割令牌]
B --> C{令牌类型}
C -- 数字 --> D[压入栈]
C -- 运算符 --> E[弹出操作数]
E --> F[执行运算]
F --> G[结果压入栈]
D --> H{是否还有令牌}
G --> H
H -- 是 --> B
H -- 否 --> I[输出栈顶结果]
具体步骤如下:
1.
输入表达式
:用户通过
calc
变量输入一个由逗号分隔的逆波兰表达式,例如
"3,1,-,3,21,5,*,+,/"
。
2.
分割令牌
:使用
$(subst)
函数将逗号替换为空格,然后使用
$(foreach)
函数遍历每个令牌。
3.
判断令牌类型
:使用
seq
函数判断令牌是数字还是运算符。
4.
数字处理
:如果是数字,使用
pushd
函数将其解码并压入栈中。
5.
运算符处理
:如果是运算符,使用
pop
函数弹出栈顶的两个操作数,执行相应的运算,然后将结果压入栈中。
6.
重复处理
:继续处理下一个令牌,直到所有令牌都处理完毕。
7.
输出结果
:最后,栈中只剩下一个元素,即为表达式的计算结果。
算术库的应用场景
虽然在GNU Make中实现算术功能不是其主要用途,但在某些特定场景下,这种实现可以发挥作用。
- 条件判断 :在Makefile中,可以使用算术比较函数进行条件判断,例如根据某个变量的值决定是否执行特定的规则。
ifeq ($(call gte,$(var1),$(var2)),)
# var1 < var2 的情况
else
# var1 >= var2 的情况
endif
- 动态生成规则 :可以根据算术运算的结果动态生成Makefile规则,例如根据文件数量生成不同的编译规则。
file_count := $(call decode,$(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST)))
if $(call gte,$(file_count),10)
# 处理文件数量大于等于10的情况
else
# 处理文件数量小于10的情况
endif
注意事项和扩展建议
在使用上述算术库和计算器时,需要注意以下几点:
-
性能问题
:递归实现的除法函数
divide在处理大数字时可能会导致性能问题,因为递归调用会增加计算时间和内存开销。可以考虑优化递归算法或使用迭代方法。 -
数字范围限制
:目前
encode函数只能处理最大为65536的数字,可以通过增加input_int中x的数量来扩展数字范围,但这会增加内存使用。 - 错误处理 :虽然在减法函数中添加了下溢警告,但在其他函数中可能还存在未处理的错误情况,例如除法时除数为0的情况。可以添加更多的错误处理代码来提高程序的健壮性。
扩展建议:
-
添加更多运算符
:可以扩展算术库,添加更多的运算符,如取模、幂运算等。
-
支持浮点数
:目前的实现只支持整数运算,可以考虑扩展为支持浮点数运算。
总结
通过本文的介绍,我们了解了构建过程中处理器数量与加速比的关系,掌握了
$(wildcard)
函数的递归使用和确定当前Makefile位置的方法,还实现了GNU Make中的算术功能和一个简单的逆波兰表达式计算器。这些技巧和方法在实际开发中可以帮助我们提高构建效率和解决一些复杂的问题。虽然在GNU Make中实现算术功能不是其主要用途,但在某些特定场景下,这种实现可以发挥作用。同时,我们也需要注意性能、数字范围和错误处理等问题,并可以根据实际需求进行扩展。
希望本文对你有所帮助,如果你在使用过程中遇到任何问题,欢迎留言讨论。
超级会员免费看
2

被折叠的 条评论
为什么被折叠?



