作者

Sales Engineer at Intersystems
文章 Jeff Liu · 八月 25 5m read

GAIA数据扫描挑战赛——……的回归

大约25年前,我曾在当地一所顶尖的高科技职业学校/学院担任讲师。👨‍🏫

我曾教授多门课程,包括“C语言入门”以及“C语言进。我承认,从那以后我几乎没怎么碰过C语言,接触得也相当少。 

自然,我大量使用ObjectScript,也接触过其他语言,比如SQL(包括一些厂商专有的变体)、Java和C#,最近还用了不少Python,而在前端开发中则主要使用各种基于JavaScript的框架。

但 C 语言呢?或许在过去一年里,我曾稍微帮过女儿一点忙——当时她正在大学攻读计算机科学专业的大一,我指导她处理指针、内存分配与释放等内容,但也就仅此而已。

就在这时,首届InterSystems员工编程挑战赛来了——任务是处理欧洲航天局(ESA)GAIA项目的数据🛰️,而对于“基准测试”这一赛项,我知道自己将不得不重新温习C语言的知识和经验……

 

你们中有些人可能知道,随着我们将 IRIS 数据平台推向(甚至超越)极致速度和优化的极限,该语言和引擎的多个部分在后台都是用 C 语言实现的,位于 IRIS 内核之中(对于非常敏感的领域,甚至使用了汇编语言……)。 因此我深知,要想让这个项目以最佳速度运行,就得从我的C语言“法宝箱”里翻出一些绝招……

幸运的是,我身边有 Claude Code 相助,毕竟我的 C 语言技能已经积了厚厚一层灰尘,不过我想这大概就像骑自行车一样(话又说回来……嗯……我和自行车嘛……🚳)。

于是,我通过libdeflate 库 熟悉了gzip 文件的 处理方法(因为 GAIA各纪元的光度测量文件是以 gzip 压缩的 CSV 格式提供的)。 

由于基准测试将涉及多核系统,我选择利用OpenMP框架对不同文件进行并行处理(同时按文件大小进行了排序,并从最大的数据流开始处理,以优化任务执行)。

在解析 CSV 文件时,我必须兼顾各种不同的值类型,这在 C 语言中颇具挑战性,因此需要进行各种值检查和转换

更多详情请参阅 Open Exchange 应用(及 GitHub)中的 Readme 文件。

需要注意的是,在 IRIS 环境中可以通过多种途径与 C 语言交互,例如使用$ZF Callout机制,但我选择在这个项目中通过 Python 进行操作。从性能角度来看,这种方式的影响可以忽略不计,而且如果 C 语言编译遇到问题,还可以“回退”到原生 Python 代码处理。

由于本次比赛的另一项提名是“最短代码”(即“代码高尔夫”比赛),尽管我承认自己并不是这类比赛的忠实粉丝…… 但我还是尝试了一下,这次同样选择了 Python。我为所有变量都起了非常简短的名称,甚至使用了一些“别名”技巧,还利用了 Python 的csv库来读写所需的文件。你可以在同一个项目中的 profiles\shortest 目录下找到该代码。

因此,这次练习让我跳出了日常的工作领域,唤醒了我的一些旧习惯,但确实是一次很酷的体验。


第二轮 🎥2️⃣

在探索优化和调整性能的方案时,我意识到自己曾做过两个天真的假设:

  1. 第一个与我想利用的核心数量有关。起初我认为留出一些余地、不占用所有核心是个明智之举。

    因此,我写了类似下面的代码来计算“工作线程数”,留出 25% 的核心闲置:

	cores = len(os.sched_getaffinity(0)) 
    	...
	return max(1, cores - max(1, cores // 4))

但经过一些测试后,我意识到这是一个错误的假设,对于这个基准测试场景,还是“贪婪”一点😋,全部占用为好……

这样做带来了一定的(稳定且可测量的,尽管幅度不大)性能提升。

  1. 第二点与文件访问有关。最初我只是从容器外部访问文件(绑定挂载)——但预先将文件导入容器存储后,容器便可通过其自身的文件系统访问这些文件。这已经产生了显著的影响

    因此,现在我在 Dockerfile 中添加了以下内容:

	RUN mkdir -p /tmp/gaia_in && cp data/in/*.gz /tmp/gaia_in/ && \ 
    	chown -R irisowner:irisowner /tmp/gaia_in
	And in my Python runner I have this now:
	LOCAL_IN = Path("/tmp/gaia_in")

此外,关于代码高尔夫(Code Golf)的实现方法,感谢 @Manel Trèmols在其代码高尔夫博文中(尽管他使用的是 ObjectScript,而我使用的是 Python)比赛规则之一所做的“解读”,这让我豁然开朗。

当我最初读到这条要求时:

生成一个包含 source_id、bp_min_flux、bp_max_flux、rp_min_flux、rp_max_flux、percentage_change 列的 CSV 文件

我当时理解为输出文件需要包含一行包含列名的标题行。但重新阅读这条说明后,确实明白这仅仅意味着文件需要包含这些列( 数据本身),而并不一定需要包含列名……

因此,如果“采纳”这种理解,我就能在代码高尔夫中节省相当大一部分代码量…… 😁😇


第三次尝试 🎥3️⃣

后来我意识到,我的天真不仅体现在上面的例子中,我还存在一个更根本的误读,这可能会对性能产生更大的影响。

事实上,这一点虽然没有直接出现在题目说明中,但在比赛模板中RunScript 例程的“伪代码”结构里却有非常明确的说明:

Run() 
{ 
 #; extract files if necessary from .data/in to .data/temp
  
 #; Start time 
 Set startTime = $ZHOROLOG

你可以看到“如有必要,解压文件……”这条注释,而只有在之后,“计时才开始……”

(起初我误读了这条注释,以为“解压文件……从……到……”仅指移动文件,而非解压/解压缩意义上的“解压”,而实际上在此语境下,“解压”确实就是这个意思……)

而我当时将解压操作放在了“计时区域”内,这造成了巨大的“时间浪费”,因为实际上这部分操作占用了大部分时间……

因此,我将代码拆分,让解压/提取操作在“开始时间”标记之前完成,而“开始时间”标记之后仅包含实际的文件处理。

不出所料,这进一步大幅缩短了处理时间 🚀