Nim Apocrypha,第一卷 一世

2020-12-23 21:30:47

问候尼姆冒险家们!您将在下面找到16个便捷的Nim提示和放大器;我在今年开发中型GUI程序Gridmonger(和相关库)时遇到的技巧。其中一些是鲜为人知或未记录的Nim功能或标准库函数,一些是该语言某些粗糙边缘的变通办法,还有一些有用的技术,我在论坛上了解到或由我自己发明。

所有技巧均适用于Nim 1.4.2及更高版本,并且大部分适用于C后端。希望您会在这里找到有用的东西,这将使您与Nim在一起的时光更加有趣和富有成效!

Nim吹捧自己是一种能够生成小型本机二进制文件的语言。但是,仅仅使用nim c -d:release天真地编译某些程序并查看文件大小会让您怀疑该声明的有效性!

Gridmonger的发行版本重约12兆。如果你问我,除了小东西什么都不要!当然,它静态地链接了GLFW和NanoVG以及许多其他Nimlibraries,但是从透视的角度来看,REAPER(用C / C ++编写的高级而复杂的DAW)的大小大致相同,约为13兆!当然,我的程序比REAPER简单得多,那么这里发生了什么?

事实证明,默认情况下,Nim二进制文件中包含gcc调试符号,这可能会导致大小增加2到5倍!幸运的是,我们可以使用gcc附带的strip命令轻松删除它们。

如何尝试使用--opt:size优化小型可执行文件?当然,这将产生大约30%的二进制文件,但以潜在的运行时性能损失为代价。因此,尽管它不一定总是最佳选择,但是您可以始终使用它,并去除调试符号以节省更多成本。

最后,如果您想产生尽可能小的二进制文件,那么使用UPX之类的可执行打包程序对其进行压缩可能只是问题所在。

重要的是要注意,二进制文件中调试符号的存在不影响其运行时性能。这是因为在正常使用期间,只有在调试时,调试符号才会加载到内存中。严重的是,它仅影响可执行文件在磁盘上占用的空间。

如果您想知道,如果您使用-d:debug或--stacktrace = on--linetrace = on对其进行了编译,您仍然可以获得带剥离二进制文件的Nim堆栈跟踪(在下一个技巧中有更多介绍)。

使用-d:release创建一个发布版本可以提高速度,但是要以关闭堆栈跟踪为代价。如果您要尝试为最终用户实现崩溃报告机制,则可能会遇到问题,该机制涉及将您的主要方法包含在try / except块中,并将带有堆栈跟踪的异常写入日志文件。

幸运的是,您无需将调试版本交付给用户即可。您仍然可以使用-d:release以获得最大的优化收益,同时使用--stacktrace = on --linetrace = on重新打开堆栈跟踪。生成的二进制文件的性能可能会稍慢一些,但要知道,生命只在于正确的权衡。

Nim默认为refc垃圾收集器(用于循环收集的带有标记和清除阶段的递延引用计数),即使对于软件实时要求,也可以很好地工作。但是您是否知道在1.2.x中引入了全新的GC,在大多数情况下可以减少内存占用并提供更好的性能?

这是ARC,它是完全确定性的“自动引用计数”垃圾收集器。切换到ARC就像向编译器提供--gc:arc选项一样容易。它是大多数程序的直接替代品。根据您的程序,您可能希望将其与--deepcopy:on--hint [Performance]:off选项一起使用。

ARC具有许多其他好处,包括硬实时支持,线程之间的共享堆以及简化C FFI。请注意,ARC无法处理循环数据结构。但是它有一个叫做ORC的老大哥,为此增加了支持。

您可以在Nim网站上的出色博文中了解有关ARC和ORC的更多信息。

使用VisualStudio可能更容易为Windows可执行文件设置图标,但是无论如何,这是我使用的gcc / MinGW的说明。

首先,您需要生成一个.ico文件,其中包含多种分辨率的图标图像。有很多在线工具可以使用,还有ImageMagick,这不在本文讨论范围之内。

拥有图片后,您需要创建引用图标文件的资源定义.rcfile。我在这里使用了appicon作为ID,但是您可以使用其他任何字符串,都没关系:

下一步是从.rc文件创建资源对象文件(windres包含在MinGW中):

这将是一个相当短的过程,但是我花了一些时间才弄清楚。结果表明,当您通过将-mwindows传递给链接程序(-L:-mwindows)与Windows库进行链接时,将无法再打印带有回显的控制台内容。

“但这不是一个有用的技巧,当然您需要在GUI程序中链接Windowslibs!”,我听到您说。好吧,不一定。例如,在GLFW应用程序中,在很多情况下,您无需链接到Windows库就可以摆脱困境。例如,在Gridmonger中,我只需要-mwindows,这样我就可以打开标准的打开和保存系统对话框。在调试版本中,我没有针对Windows库进行链接,而是有条件地将对dialogfunctions的调用转为no-ops,然后可以将调试打印到控制台。

using关键字是一个非常有用但经常被遗忘的语言功能。创建方法调用样式API或传递上下文对象时,它有助于减少冗余。

#不使用proc getFloor *(l:Level,row,col:Natural,a:var AppContext):Floor = ... proc setFloor *(l:Level,row,col:Natural,f:Floor,a: var AppContext)= ...#通过l使用`using`:使用a:var AppContext级别proc getFloor *(l; row,col:Natural; a):Floor = ... proc setFloor *(l; row,col :自然,f:底数; a)= ...

您是否知道标准库具有一个浏览器模块,其唯一目的是以跨平台的方式在OS默认浏览器中打开URL?事实上,我已经使用Nim已有4年了,最近我已经了解了这一点!

如果您要从桌面应用程序将用户导航到程序的网站,或打开本地HTML文档,这将非常方便。

同样,如果您需要以跨平台方式处理配置文件或程序数据,则os模块中的getHomeDir()和getConfigDir()超级方便。

不管复杂的现代开发工具可能是什么,仅将内容回显到控制台仍然是调试程序的最简单,最快的方法之一,但是编写回声" foo:"之类的东西,在foo的第一百次就变得陈旧了快速。标准制糖模块中的dumpmacro可以提供帮助。

进口糖var a = 42 s =&frobnicate" x = 7 dump(a)dump(s)dump(a + x)#打印:#a = 42#s = frobnicate#a + x = 49

尽管您可以使用常规时间模块来测量经过时间,但要正确执行该操作,您确实需要单调计时器。最近,这种东西已经以std / monotimes的形式添加到标准库中。

import os import std / monotimes导入时间proc durationToFloatMillis *(d:Duration):float64 = inNanoseconds(d)。 float64 * 1e-6让t0 = getMonoTime()睡眠(10)#做一会儿让d = getMonoTime()-t0回波durationToFloatMillis(d)

Openarrays是一种方便的Nim功能,使您可以编写可通过统一的openArray类型接受数组或序列的过程。手动忘记告诉您的是,系统模块中有一堆重载的toOpenArray和toOpenArrayByte方法,以帮助从数组,seq,字符串和其他(令人惊讶的)open数组创建openarray“切片”。

一个特别有用的函数是在UncheckedArrays上运行的函数,这对于将C库中的内存块视为Nim代码中的openarrays非常有用。

说到系统模块,它充满了文档中未曾提及的有用内容。请确保不时浏览一下功能列表,我保证您每次都会找到感兴趣的内容。

有时,您必须诉诸C风格的指针算术(尤其是在与C库和数据结构接口时),而Nim的类型安全本质并没有那么简单。以下模板使此类任务更加容易(当然,可以引入许多其他变体;这对读者来说是一个练习)。

模板`++`[A](a:ptr A,偏移量:int):ptr A = cast [ptr A](cast [int](a)+ offset)模板`-`[A](a,b :ptr A):int =强制转换[int](a)-强制强制转换[int](b)

Nim的致命弱点之一是模块系统在处理循环类型依赖项时相对僵硬。在一个复杂性很高的项目中,您将代码分成多个子模块,您迟早会无一例外地遇到这个问题。长话短说,最好的解决方法是尽早创建一个包含所有此类循环类型定义的通用模块。然后,您可以将此通用模块包括在所有其他子模块中。

这种方法的最大缺点是,所有共同定义的东西都必须是公开的。但是嘿,谁告诉你这个世界上有什么完美的呢?

通过将对象或元组的一部分提升到当前作用域中,带有宏的tiny宏对于减少冗余非常有用。显示起来可能比解释起来容易:

类型为Widget = object backgroundColor:字符串frontColor:字符串Window =对象标题:字符串inputField:Widget#不带`with` var mainWindow = Window()mainWindow。 inputField。 backgroundColor ="黑色" mainWindow。 inputField。前景色="红色" #在mainWindow中使用`with`。 inputField:backgroundColor =" black"前景色="红色" #您也可以嵌套!使用mainWindow:title =" Qux"与inputField:backgroundColor =" black"前景色="红色"

即使配备了上述宏,通常也需要对嵌套对象层次结构的某些部分进行一些别名(引用),以提高可读性。这在实际的应用程序和UI编程中经常出现。考虑以下:

现在想象一下,大多数程序基本上都以某种方式在此应用程序上下文中运行。这一切都变得非常多余,并且很快变得难以理解。而且由于Nim的复制语义,您无法设置C ++样式引用(除非您一直在各处使用ref对象,但这并不总是最佳选择)。

模板别名*(newName:未类型化,调用:未类型化)=模板newName():untyped =调用别名(doc,g_app.doc)别名(map,doc.map)map。级别[doc。 currLevel]。 setFloor(row,col,fEmpty)

更具可读性! (当然,通常您会像在此简单示例中那样使用这些别名一次或两次以上。)

引入适当的C ++样式引用的另一种方法是addr pragma最近添加的(并且完全未记录):

导入std / decls var a = 5 var b {。 byaddr。} = a b = 3 echo a#打印3

您应该将最好的保存到最后,否则他们会说。好吧,这是关于节省内存的重要时间!更有经验的C / C ++程序员现在可以回家,因为他们应该已经知道了这一切-其余的课程仍然存在。

假设我们在程序中有一个对象来保存大型(ish)网格/矩阵的单元格的属性:

作为我们这种注重资源的开发人员,我们尽职尽责地关注我们的总内存占用量:

类型Direction =枚举East,West,North,South类型Cell * =对象b:字节d:方向echo sizeof(Cell)#打印2 var a:array [500 * 500,Cell] echo sizeof(a)#打印500000

仍然完全没有兴趣。枚举由最小可能的整数类型表示,因此我们的枚举长度为1个字节,这使我们的总内存需求增加了一倍。

好吧,是时候冒险一些了!我们将在thebyte和枚举之间插入一个字符串:

Nim字符串只是一个指向字符序列的指针,这意味着我们期望对象以sizeof(string)== sizeof(pointer)== 8个字节(64位)增长。对?对???!!我希望那些曾经社交过的,轻松的高级语言的人们在这一点上强烈地同意我的观点。

不完全是我们想要的答案,是吗?在久经考验的良好编码条件下,让我们开始随机更改狗屎,以期有所改善!

类型Cell * =对象s:字符串b:字节d:方向echo sizeof(Cell)#打印16 echo sizeof(a)#打印400000

嗯,我们只是将字符串放在首位,从而将总内存占用量减少了1/3。 WTF正在这里吗?

Nim对象可编译为C结构(带有C后端),这些结构由标准中严格定义的顺序和内存对齐要求控制。每种CPU架构的确切情况略有不同,但是onx86 / x64的以下主要规则适用:

数据类型必须根据其位宽进行对齐以实现最佳访问,因此64位值必须在8字节边界(地址可被8整除)上对齐,32位值在4字节边界上对齐,依此类推。

必须在结构末尾填充结构,以便将它们放置在连续数组中时,其所有成员均按照上述最佳方式对齐。不切实际的用语是指该结构的总大小被填充为它所包含的最宽数据类型的最接近的整数倍。

在任何情况下(为了优化填充或任何其他原因),不允许编译器对结构字段进行重新排序,原因是这将破坏许多低级代码。

x86 / x64支持不对齐访问(这不会使程序崩溃),但是会导致性能下降。

另一个重要的背景信息是C99规定,对于实现所支持的任何数据类型,必须为malloc分配的内存块正确对齐。因此,在结构的第一个元素之前永远不需要填充。实际上,您可以假设分配的内存块在x86上始终是8字节对齐的,而在x64上始终是16位对齐的,而与操作系统无关。

因此,有了这些不可思议的知识,就很难弄清发生了什么:

#最坏的情况(中间的字符串)-填充类型为Cell的24个字节* =对象b:字节#1个字节+ 7个填充字节s:字符串#8个字节(8字节对齐)d:方向#1个字节+ 7个填充字节(以确保下一个数组是#个元素是8字节对齐的)#更好(开始时是字符串)-填充类型为Cell的16个字节* = object s:字符串#8个字节(由于malloc而对齐8个字节)b:字节#1字节d:方向#1字节+ 6个填充字节(以确保下一个数组#元素是8字节对齐的)

填充要求的一个有趣的结果是,我们可能还会在那些未使用的填充字节中存储一些有用的东西(只要我们注意不要过冲):

类型Cell * =对象s:字符串#8字节b:字节#1字节d:方向#1字节x:int16#2字节(偏移量10,2字节对齐)y:int32#4字节(偏移量12、4-字节对齐)echo sizeof(Cell)#仍然是16个字节!

然后是可以应用于对象的packedpragmatoo,以有效地禁用填充,但这应该限于低级代码或必须与C库进行接口的情况,因为它不能很好地与GC的内存配合使用(该手册解释了为什么这样做是这样)。

这绝对是一个非常有趣的主题,如果您想进一步探索,请查看以下文章:

这么长的人,希望您在这里找到了一些有趣的东西。洗手,戴口罩,在节日期间不要酒后开​​车,并保持安静! (就是一个字吗?我想现在是……)