Scala 中柯里化函数、部分应用函数和 'normal' 函数之间的性能特征是什么?
What are the performance characteristics between curried, partially applied, and 'normal' functions in Scala?
我在这里看了一下:Scala currying vs partially applied functions,但那里的答案更多地回答了 Scala 中柯里化、部分应用和普通函数之间的功能和语义差异。
我有兴趣了解这些可用于函数的不同技术之间是否存在任何性能考虑因素,即...
如果我们以普通函数的性能为基础:
def add3(a: Int, b: Int, c: Int) = a + b + c
add3(1, 2, 3)
然后比较:
// First
(add3 _).curried(1)(2)(3)
// Second
val add2 = add3(1, _: Int, _: Int)
val add1 = add2(2, _: Int)
add1(3)
// Third
def add3(a: Int)(b: Int)(c: Int) = a + b + c
add3(1)(2)(3)
如果我发现了一些性能不佳的代码(无论是在速度还是内存使用方面)并且我看到在所述代码段中发生了很多柯里化或部分应用程序,我可能需要注意哪些事情?
在 Haskell 中,例如,我会查看正在生成和徘徊的 thunk 数量。我假设 Scala 使用类似的方法来传递部分应用和柯里化的函数,了解 Scala 如何处理这些事情的细节将很有价值。
我使用 scala-to-java tool 将您的代码段转换为 java,结果如下:
简单的函数调用被转换为类似的函数调用:
def add3(a: Int, b: Int, c: Int) = a + b + c
add3(1, 2, 3)
结果:
public final class _$$anon {
private int add3(final int a, final int b, final int c) {
return a + b + c;
}
{
this.add3(1, 2, 3);
}
}
"First" 片段:(add3 _).curried(1)(2)(3)
本质上被转换为:
((Function3<Integer, Object, Object, Object>)new _$$anon$$anonfun._$$anon$$anonfun(this)).curried().apply(BoxesRunTime.boxToInteger(1)).apply(BoxesRunTime.boxToInteger(2)).apply$mcII$sp(3);
我省略了样板文件。这里发生的是围绕 add3
创建包装函数 class,然后调用 class 的方法 curried
,然后对结果调用三次 apply
以前的应用程序。您可以查看 curried
here 的来源。它的作用,就是return几个高阶函数(function that return function)。
因此,与 "case 0" 相比,为单个柯里化调用创建了几个额外的函数包装器。
"Second":
// Second
val add2 = add3(1, _: Int, _: Int)
val add1 = add2(2, _: Int)
add1(3)
我不会提供整页的转译输出。如果需要,请勾选 here。本质上发生的是 add2
和 add1
函数包装器 class 是使用采用相应数量参数的单一方法生成的。因此,当您调用 add1(3)
时,它会调用 add2
,而 add2
又会调用 add3
。生成的包装器实际上是单例,因此开销仅限于几个函数调用。
第三个:
def add3(a: Int)(b: Int)(c: Int) = a + b + c
add3(1)(2)(3)
再次转译为简单的函数调用:
public final class _$$anon {
private int add3(final int a, final int b, final int c) {
return a + b + c;
}
{
this.add3(1, 2, 3);
}
}
但是如果你试图以柯里化的方式使用它,例如,像这样val p = add3(1) _
,一个功能包装器class将被额外生成。
我在这里看了一下:Scala currying vs partially applied functions,但那里的答案更多地回答了 Scala 中柯里化、部分应用和普通函数之间的功能和语义差异。
我有兴趣了解这些可用于函数的不同技术之间是否存在任何性能考虑因素,即...
如果我们以普通函数的性能为基础:
def add3(a: Int, b: Int, c: Int) = a + b + c
add3(1, 2, 3)
然后比较:
// First
(add3 _).curried(1)(2)(3)
// Second
val add2 = add3(1, _: Int, _: Int)
val add1 = add2(2, _: Int)
add1(3)
// Third
def add3(a: Int)(b: Int)(c: Int) = a + b + c
add3(1)(2)(3)
如果我发现了一些性能不佳的代码(无论是在速度还是内存使用方面)并且我看到在所述代码段中发生了很多柯里化或部分应用程序,我可能需要注意哪些事情?
在 Haskell 中,例如,我会查看正在生成和徘徊的 thunk 数量。我假设 Scala 使用类似的方法来传递部分应用和柯里化的函数,了解 Scala 如何处理这些事情的细节将很有价值。
我使用 scala-to-java tool 将您的代码段转换为 java,结果如下:
简单的函数调用被转换为类似的函数调用:
def add3(a: Int, b: Int, c: Int) = a + b + c add3(1, 2, 3)
结果:
public final class _$$anon { private int add3(final int a, final int b, final int c) { return a + b + c; } { this.add3(1, 2, 3); } }
"First" 片段:
(add3 _).curried(1)(2)(3)
本质上被转换为:((Function3<Integer, Object, Object, Object>)new _$$anon$$anonfun._$$anon$$anonfun(this)).curried().apply(BoxesRunTime.boxToInteger(1)).apply(BoxesRunTime.boxToInteger(2)).apply$mcII$sp(3);
我省略了样板文件。这里发生的是围绕
add3
创建包装函数 class,然后调用 class 的方法curried
,然后对结果调用三次apply
以前的应用程序。您可以查看curried
here 的来源。它的作用,就是return几个高阶函数(function that return function)。因此,与 "case 0" 相比,为单个柯里化调用创建了几个额外的函数包装器。
"Second":
// Second val add2 = add3(1, _: Int, _: Int) val add1 = add2(2, _: Int) add1(3)
我不会提供整页的转译输出。如果需要,请勾选 here。本质上发生的是
add2
和add1
函数包装器 class 是使用采用相应数量参数的单一方法生成的。因此,当您调用add1(3)
时,它会调用add2
,而add2
又会调用add3
。生成的包装器实际上是单例,因此开销仅限于几个函数调用。第三个:
def add3(a: Int)(b: Int)(c: Int) = a + b + c add3(1)(2)(3)
再次转译为简单的函数调用:
public final class _$$anon { private int add3(final int a, final int b, final int c) { return a + b + c; } { this.add3(1, 2, 3); } }
但是如果你试图以柯里化的方式使用它,例如,像这样
val p = add3(1) _
,一个功能包装器class将被额外生成。