本文最后更新于:2 years ago

Pre-everything:

我已经在图书馆听歌、冲浪将近一个半小时了,实在是懒得自学QT。而唯一剩下的一个小作业老师又没有讲到,我又不想看大作业(sigh),宿舍要等到5点才结束装修,所以就写下这个昨天就想写的小note好了。

就算这些废话和这篇博客毫无关系,我也还是要写在开头。

小作业的题目如下:

调整三个for循环的顺序,生成所有可能的遍历方式,同时测试在不同规模下循环体的运行时间并加以分析:

for (int i = 0; i < N; ++i)
    for (int j = 0; j < N; ++j)
        for (int k = 0; k < N; ++k)
            c[i][j][k] = a[i][j][k] + b[i][j][k];

(主要就是探究一下内存的cache机制)

总的来说,并不是一个很难完成的task。跟随群里dalao的带领,我们主要试图通过宏和shell script解决两个问题:

(1)如何避免代码重复?(也即,如何避免6个3重循环?)

(2)如何改变参数后多次运行,以达到测试的目的?

Step 1: 仿函数宏——避免代码重复

群里有同学提到了使用##, #完成所需的任务。

我一开始的思路是使用一个[6][3]的二维数组来完成这一任务:

#define f(x) (var##x)

int permute[6][3] = {{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}};

int main() 
{
    for (int i = 0; i < 6; ++i) {
       for (int f(permute[i][0]); ; )
           for (int f(permute[i][1]); ; )
               for (int f(permute[i][2]); ; )
                  // do something
}

然而非常悲壮地,它报错了。原因从一个化简的下面程序即可看出来:

#include <iostream>

using namespace std;

#define f(x) (var##x)

int main()
{
	int a = 1;
	int var1 = 1;
	cout << f(a) << endl;
}

[Error] ‘vara’ was not declared in this scope.

嗯…一般的仿函数宏不是都会把变量的值带进去吗?为什么这里是把变量的名字a带进去了呢?

这是因为#/##是宏中比较特别的两个字符,一般来说不会进行替换。

下面是一段我没有看懂也不想仔细看的替换规则:

规则1:实参替换。
本条规则描述带参数的宏的替换过程。

对于宏定义中的形参,在替换列表中,如果不是作为#或##的操作数,那么将对应实参完全
展开(相当于对实参进行求值),然后将替换列表中的形参替换掉.如果是#或##的操作数,

那么不进行替换。

规则2:多次扫描。
在所有的形参替换为实参后,对结果进行再次扫描,如果发现还有可替换的宏,则进行替换,
否则中止。

规则3:递归替换抑制。
如果在替换列表中发现当前正在展开的宏的名字,那么这里不进行替换.更进一步,在嵌套
的替换过程中发现已经替换过的宏的名字,则不进行替换。

规则4:递归预处理抑制。
如果替换后的结果形成预处理指令,则不执行这条预处理指令。

就我粗浅的理解,想要通过变量名访问、让变量的值被传进参数里面这条路在这个规则下应该是行不通了。

正当我又试了几种其它方法都行不通、打算放弃之际,突然灵光一现——想到了一个小点子!(实际上是一种折衷方案,并没有完全解决之前的问题)直接使用变量去定义包含for循环的宏

#define FOR_LOOP(i) for (int var##i = 0; var##i < n; ++var##i)
#define FOR(i) FOR_LOOP(i)

#define TIMECOUNT(i, j, k) { startClock = clock();\  //\表示换行但仍在宏内
  FOR(i) { FOR(j) { FOR(k) { a[var0][var1][var2] = b[var0][var1][var2] + c[var0][var1][var2]; } } }\
  endClock = clock();\
  timeCount = (double)(endClock - startClock) / CLOCKS_PER_SEC;\
  outFile << " " << setprecision(6) << timeCount;\
}

const int n = 100; 
// a bunch of other definitions and initializations

int main()
{
  TIMECOUNT(0, 1, 2); TIMECOUNT(0, 2, 1);  //ijk & ikj
  TIMECOUNT(1, 0, 2); TIMECOUNT(1, 2, 0);  //jik & jki
  TIMECOUNT(2, 0, 1); TIMECOUNT(2, 1, 0);  //kij & kji
}

这样的代码相对来说还是比较好看的!

当然,这并没有解决之前的问题。如果这是一个5重循环,下面就会有120条TIMECOUNT语句,仍旧是相对比较丑陋的。

我们先跳过这一问题,进入到改变数据规模n进行测试的环节(下一环节其实提供了一个解决该问题的备选方案)。

Step 2:使用shell script进行测试

接下来问题又来了,这个文件中的数据规模n是一个常数。我们在.cpp文件中好像没有办法方便地循环改变它(至少我没有想到办法)。

然后我想到了上学期OOP课程做课程report学过一点的shell script。如果把常数变成一个宏,定义在CONST.h里:

// CONST.h
#define n 100

那么我们只需要在shell script里面重写这个文件,然后重新编译运行这个文件就好了:

#!/bin/bash

n=0

# write the head of table
echo "n ijk ikj jik jki kij kji" > table.txt
# > 意味着写入文件头部,如果文件不存在则创建文件

# for循环
for n in {32..300}
do
	# write the new const n to file CONST.h
	sed -i "/#define n/c#define n $n" CONST.h
	# sed -i(插入) "/字符串1/c字符串2" 文件名
	# 则会搜索与字符串1匹配的行,把整行内容替换为字符串2
	
	# compile and run with the new const n
	echo $n
	make
	./main
done

我们希望main.cpp中把内容写到文件的尾部:

outFile.open("table.txt", ios::app);

到此就完工啦。

事实上,仔细一想,使用shell script也可以解决之前的问题:可以从另一个写好数组内容的文件中读取数据,然后利用这些数据写出main.cpp中应该有的120条的TIMECOUNT语句。(属于曲线救国,本质上还是没有解决这个问题)

还有一个遗留问题:这样每次都要重新编译链接,比较耗时。网上查了一下可以使用配置文件,但我也并不清楚要怎么弄,就留给下次再研究啦。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!

Diary: 2020/09 Previous
OOP复习笔记-Part II Next