Scala 单例对象的三个应用

一 进度条

有时候我们在编写代码时希望能够查看运行时间,最简单的方法:

1
2
3
4
5
6
def main(args: Array[String]) {
val startTime: Long = System.currentTimeMillis
待测试的代码块
val endTime: Long = System.currentTimeMillis
System.out.println("程序运行时间: " + (endTime - startTime) + "ms")
}

但这个方法显然不是那么优雅。如果你在使用Scala的时候注意运行log,你会发下进度条更漂亮直观的。

1
2
3
4
5
6
7
[Runtime] SpinalHDL v1.3.9    git head : a4cb4aadf0820174c1b48023bfcd3e9981de1d4a
[Runtime] JVM max memory : 1820.5MiB
[Runtime] Current date : 2020.01.23 10:44:55
[Progress] at 0.000 : Elaborate components
[Progress] at 0.513 : Checks and transforms
[Progress] at 0.705 : Generate Verilog
[Done] at 0.806

实现非常简单,首先得创建一个单例对象,单例对象一旦被实例化以后,startTime就会确定,不管Driver后面被调用多少次只会用到第一次被实例化的对象,startTime也不会改变。

1
2
3
4
object Driver {
val startTime = System.currentTimeMillis()
def executionTime: Double = (System.currentTimeMillis - startTime) / 1000.0
}

再创建一个名叫“MyProgress”的用户接口, 每次执行“MyProgress”时调用单例对象Driver的executionTime打印当前时刻。

1
2
3
4
object MyProgress {
def apply(message: String) =
println(s"${MyLog.tag("Progress", Console.BLUE)} at ${f"${Driver.executionTime}%1.3f"} : $message")
}

以下是一个基带仿真的case,每个关键节点调用MyProgress, 每步耗费的时间一目了然(单位s)。

1
2
3
4
5
6
7
8
9
[Progress] at 0.000 : Coherence correlation start
[Progress] at 0.076 : DopplerOffset HertzNumber(250.0)
[Progress] at 0.296 : Sample done
[Progress] at 0.481 : add AWGN noise
[Progress] at 0.488 : Normalized rx power to 0 dB
[Progress] at 0.489 : Receive data with AWGN, Sampled at 2.048/40.96MHz
[Progress] at 0.529 : Quantization to 2bit done
[Progress] at 0.538 : 2 bit AD data remaping done
[Progress] at 2.243 : carrier Mixing done

我也听到有人抱怨Rocket生成时间巨长,自己也搞不清楚时间到底耗费在什么地方,有可能是编译,也有可能是逻辑代码,也有可能是firrtl的解析生成。 现在你就可以用MyProgress来诊断瓶颈到底出在哪儿。

我自己用Scala编写算法参考平台时就遇到一个怪异的问题,按理说Scala是静态语言比Python更快,但是大致相同的算法,运行时间奇慢无比,后来就用MyProgress的进行二分法,找到问题的症结,我在打印文件时代用折叠的方式拼接字符串,foldLeft在功能上并没有问题,但是在字符串拼接是时间复杂度应该不是线性的(具体我没有深究,有兴趣的同学可以研究一下),导致很慢。

1
fp.write(content.foldLeft("")(_+"\n"+_))

正确的方法是用mkString,速度回复正常,问题解决

1
fp.write(content.mkString("("\n")))

最后这里的MyProgress正是SpinalHDL的SpinalProgress,有兴趣可以阅读其源码

二 开关

开关应用还是比较广泛,用过Matlab你就会比较熟悉

1
2
grid on
grid off

我举另外一个例子,算法在设计完功能和性能仿真以后要做定点性能仿真,定点的过程是需要修改代码,但是算法需要经常在全精度和定点之间切换调试,因为有时候性能不过,我们不清楚是逻辑的问题还是定点的问题。

当一个很大的project中定点地方可能非常多,我们不可能每次去来回修改定点代码,优雅的方式应该设置一个开关,而不是去修改代码。

首先设置一个开关单例对象

1
2
3
4
5
6
7
8
9
10
11
12
13
protected object FixSwitch {
private var switch: Boolean = true
def state: Boolean = switch
def on: Boolean = {
SpinalInfo("FixPoint Switch on")
switch = true
state
}
def off: Boolean = {
SpinalInfo("FixPoint Switch off")
switch = false
state
}

然后设置2个开关动作来改变单例对象的状态。

1
2
3
4
5
6
7
object FixOn{
def apply(): Boolean = FixSwitch on
}

object FixOff{
def apply(): Boolean = FixSwitch off
}

定点源码中加入开关状态

1
2
3
4
5
6
7
8
9
10
11
class FixData(...){
...
def fixProcess(): Double ={
if(FixSwitch.state){
fixPoint logic .....
} else {
raw
}
}
...
}

性能仿真

1
2
3
4
object PerformanceRegression extends App{
FixOff() //关闭定点,默认定点开启
Simulation5G() //代码中的定点自动失效,不需要手动修改
}

有些人发现本质上这就是设置了一个全局可变的变量而已,

1
var FixSwitchState: Boolean = true

但是我们为什么不用全局变量, 而是单例对象,留给大家思考。

三 全局默认参数

同样也是定点上的一个例子,SpinalHDL提供定点工厂函数

1
val wire5bit = wire10bit.fixTo( 6 downto 2, roundType = FLOOR, symmetric = true)

但是一个Project可能有成百上千的定点处理, 每个定点都添加roundType, symmetric显然很累赘,当然你可以选择默认参数

1
val wire5bit = wire10bit.fixTo( 6 downto 2)

不同的团队,不同的项目,可能使用不同的Round方式还有对称方式,我们希望默认参数也是可配置的。SpinalHDL UInt/SInt定点源码:

1
2
def fixTo(x: Range.Inclusive): IQ = 
fixTo(x, round = getFixRound(), sym = getFixSym())

默认的round,和 symmetric 参数通过getFixRound, getFixSym从全局获得。

一般一个工程种round 和 symmetric都是固定的,直接在SpinalConfig种设置即可。

1
2
SpinalConfig(mode = Verilog,
fixPointConfig = FixPointConfig(ROUNDUP, true))

当然也可以在通过以下方式来刷新默认配置。

1
FixPointConfig(ROUNDUP, true).flush()

具体实现参见源码

Scala3-Macro系统Tasty进展

Scala3 重新设计Macro系统,这是官网英文原文
翻译的很烂,全当学习笔记而已,仅供参考

Or: Scala in a (Tasty) Nutshell

如何迁移到 Scala 3这篇博文中提到最大的一个问题是关于宏的问题。
目前我们正在努力将Tasty和macros对齐,接下来谈一谈我们的想法.

What is Tasty?

Tasty是Scala3的高级交换格式。它基于类型化的抽象语法树
这些树在某种意义上包含了Scala程序中的所有信息。
它们表示程序的语法结构,还包含有关类型和位置的完整信息.
Tasty在语法检查之后(这样所有的类型都显式的知道了,并且隐式的东西都已经解释过了)给代码做一个快照,
但是在快照之前不会经过任何转换,因此所有的信息都不会丢失。
放语法树的文件为了紧凑会压缩优化(有点像javascript的压缩),这意味着我们可以在任何编译器运行期间
生成完整的Tasty语法树,即便是单独编译也不依赖任何其他东西。

Scala 避坑大法

  • 单引号双引号是有区别的
    单引号表示:char字符
    双引号表示:string字符

  • scala 为什么不建议用return

    1. return是命令时语句,Scala鼓励函数式编程,函数式在描述关系,而不是高速计算机怎么做
    2. return会破坏Scala的类型推断,加上return 你得显式的声明返回类型
    3. return在有些情况下使返回含义模糊
    4. scala实际上并没有真正意义上的return语句,而是又抛出异常的语法糖包裹实现的
  • 如何定义一个无限长的序列 Infinite Stream
    无限长的序列有什么用

BugList

1
2
3
4
object fix{
def on: Boolean = FixSwitch on
def off: Boolean = FixSwitch off
}
1
2
fix on
val a = 2 // is Ok
1
2
3
fix on
println("xxx") // Compile error
val a = 2

Scala 初探.二(package组织方式)

多个文件的package组织方式

1
2
3
4
5
6
//a.scala 
package com.east
import com.west._
object objectMain extends App{
println("add Function from west used in east, 1+2=%d".format(Add.add(1,2)))
}

Scala 初探.三(sbt组织scala代码1)

上一节我们用Makefile来组织scala代码,这里有更好的选择sbt来管理项目
sbt推荐将scala代码放在src路径,如下为标准的组织方式(代码还是原封使用第二节的样例代码)
总共3个文件

1
2
3
4
5
6
7
├── build.sbt
└── src
└── main
└── scala
├── a.scala
├── b.scala
└── c.scala

Scala 初探.四(sbt组织scala代码2)

package and import

1
2
3
4
5
6
7
├── build.sbt
└── src
└── main
└── scala
├── Interleave.scala
├── utils.scala
└── config.scala

对于同一目录下的两个文件,可以不用import,默认在一个package内

1
2
3
4
5
//Interleave.scala

class A {
val a = new ClassInUtils
}

Scala 初探.一(main函数)

SCALA 程序的几种运行方式

方法1

脚本式的使用,scala a.scala 可正常打印

1
2
3
4
def main() = {
println("Hello, Scala way1")
}
main()

但这种写法不能被 scalac a.scala正确编译,可能JVM对象必须要有个main入口函数