关于javascript:JS-遇到-CPU-密集型代码耗时长怎么破-来试试-GolangWASM-曲线救国

48次阅读

共计 15103 个字符,预计需要花费 38 分钟才能阅读完成。

1 写在后面

1.1 业务场景

最近遇到一业务需要,为了简述需要,假如业务场景依据全国各地天气气温数据生成气温分布图,须要在 Web 前端进行数据动静 插值 操作,大抵过程将是已有的全国天气数据依据一种算法生成新的插值数据,而后将插值数据在 Web 端利用 Canvas 渲染进去。

插值 在数值剖析的数学畛域,插值是一种预计类型,一种在一组离散的已知数据点范畴内结构新数据点的办法。在工程学和迷信中,通常有许多通过采样或试验取得的数据点,它们代表无限数量的自变量值的函数值。通常须要进行插值,即为自变量的两头值估算该函数的值。

—— 维基百科

依据业务场景,这里插值应用了 克里金法 (Kriging)算法进行插值,如果你不晓得 克里金法 算法也没有关系,后文对这个算法有介绍。

应用 JavaScript 运行这个算法的状况:通过 2000 条测试数据运行 JavaScript kriging 算法生成后果插值数据,大抵会花一分三十秒左右,这也太慢了吧?????

实现生成插值数据后,接下来将插值数据进行可视化的形式渲染进去,下图就是原始数据与通过克里金法算法生成的插值数据渲染进去的比照图

这里 克里金法(Kriging)算法不仅能够二维数据进行插值,也能够利用到三维数据上,下图来源于百度百科克里金法词条

1.2 什么是 kriging ?

这里次要介绍算法,不波及算法实现过程及推论,如果你不关怀这个算法,也能够跳过这里,不影响后文的了解。

克里金法(Kriging)在统计学中,最后在地统计学中,克里金法或高斯过程回归是一种插值办法,其插值由先验协方差管制的高斯过程建模。在先验的适当假如下,克里金法给出两头值的最佳线性无偏预测。该办法被广泛应用于空间剖析和计算机试验。

—— 维基百科

克里金算法算法插值操作次要是以下两步:

  • 利用已有数据进行数据模型训练
  • 依据输出数据预测生成插值数据

kriging 算法分类有:

  • 一般克里金(Ordinary Kriging, OK)
  • 泛克里金(Universal Kriging, UK)
  • 协同克里金(Co-Kriging, CK)
  • 析取克里金(Disjunctive Kriging, DK)
  • 混合算法

    • 回归克里金(regression-Kriging)
    • 神经网络克里金(neural Kriging)
    • 贝叶斯克里金(Bayesian Kriging)

这里算法选取一般克里金(Ordinary Kriging, OK), 下图是来源于维基百科的一般克里金算法实践根底

对于一般克里金算法实现过程及数学公式可查看维基百科 Ordinary Kriging,上面是一般克里金的模型函数(半变异函)分类:

  • Spherical – 球面半变异函数模型。
  • Circular – 圆半变异函数模型。
  • Exponential – 指数半变异函数模型。
  • Gaussian – 高斯(或正态分布)半变异函数模型。
  • Linear – 采纳基台的线性半变异函数模型。

这里咱们临时抉择 Kriging 算法函数模型为 exponential (指数半变异函数模型)。

1.3 kriging 算法实现的开源库

科学计算和数据分析这块还是用 R 语言与 Python 的人比拟多,Github 搜寻 kriging 关键字,关联相干仓库的次要语音是 RPython 的比拟多,其次顺次升高的是 C/C++JavaScriptJava

Python 开源实现的 kriging 差值算法库有:

PyKrige

  • OrdinaryKriging: 2D ordinary kriging with estimated mean
  • UniversalKriging: 2D universal kriging providing drift terms
  • OrdinaryKriging3D: 3D ordinary kriging
  • UniversalKriging3D: 3D universal kriging
  • RegressionKriging: An implementation of Regression-Kriging

    GSTools

  • Simple:Simple kriging
  • Ordinary:Ordinary kriging
  • Universal:Universal kriging
  • ExtDrift:External drift kriging (EDK)
  • Detrended:Detrended simple kriging.

pyKriging

  • Simple:Simple kriging
  • RegressionKriging: An implementation of Regression-Kriging

JavaScript 有一个实现了一般克里金的 kriging.js 开源库,实现模型函数有上面三个

  • Gaussian: k(a,b) = w[0] + w[1] * (1 – exp{ -( ||ab|| / range )2 / A } )
  • Exponential: k(a,b) = w[0] + w[1] * (1 – exp{ -( ||ab|| / range ) / A } )
  • Spherical: k(a,b) = w[0] + w[1] (1.5 (||ab|| / range ) – 0.5 * (||ab|| / range )3 )

1.4 技术路线考量

这里 kriging 算法运行工夫的长短与数据量成正相干,PythonJavaScript 实质上都是利用单核资源,算法运行时长应该差不到太多。

如果在浏览器外面进行 kriging 算法运行也是可行的,浏览器环境下运行 kriging 算法倡议应用 Web Workers 独自开一个线程跑 kriging 算法,防止以后页面窗口 JavaScript 引擎线程耗时太长造成 GUI 渲染线程阻塞,导致用户操作界面得不到响应,感触到显著的卡顿状况。

JavaScript 是单线程,GUI 渲染线程与 JS 引擎线程是互斥的,所以如果 JS 执行的工夫过长,这样就会造成页面的渲染不连贯,导致用户操作界面得不到响应。

上文提到通过 2000 条测试数据运行 JavaScript kriging 算法生成插值数据,大抵会花一分三十秒左右,为什么这么慢呢?因为kriging 算法外面须要进行大量的数学函数运算和矩阵运算,故属于 CPU 密集型操作,对于 IO 密集型劣势比拟强的 Node 相比之下劣势就比不显著了,如果采纳 C/C++RustGo 这类比拟根底的语言编写算法运行速度应该有所晋升。

如果算法运行程序的关联性比拟弱的话,那么能够利用多核 CPU 的劣势,应该能够再晋升肯定的速度,但 C/C++Java 这类语言多线程的操作比拟麻烦,恰好笔者理解点 Golang,如果利用 Golang 的并发设计 goroutine 协程机制,来并发利用多核 CPU 的劣势编写代码要简略的多,如果前面 kriging 算法做成 HTTP 服务的话,也很不便编写并发服务多人同时运行算法进行数据插值。

既然如此,那何不用 Golang 重写一下 kriging.js 这个代码呢?如果 Golangkriging 算法运行效率相比于浏览器环境 JavaScript Web Workers 下的算法运行效率比拟高的话,那么前面能够做成 HTTP 服务的形式提供给前端调用接口返回插值的数据,而后前端依据插值数据渲染出图呢。

不错思路按理可行,如果不思考返回插值的数据量大的状况及 HTTP 服务的耗时,还不错,但通过运行 JavaScript kriging 算法进行 2000 条数据插值,生成可渲染的矩阵插值数据大抵有 6-7 MB,如果这个返回到前端,这还是有点大呢,数据传输耗时还不能够疏忽呢。

那么如果把渲染出图的操作也放在服务端呢,最终返回图片格式到前端,这也可行,不过渲染数据出图如果前端定制性要求比拟高的话,那么服务端渲染出图操作的代码量比拟大。

如果依照下面的技术路线,多用户下服务端运行 kriging 算法,如果服务器多核 CPU 的资源用完了,剩下的用户只有期待排队了,这也不太现实了,如果能够让处于排队状态的用户能够抉择把 kriging 算法运行放到本人电脑上,利用本人电脑 CPU 性能决定生产效率,也行,这样多一个抉择也好。

除此之外,还能够试试利用 WebAssembly 技术嘛,将 Golang 版重写的 kriging.js 放到浏览器环境下运行,将 Go 代码编译成低级的类汇编语言的模式在浏览器外面运行,这或者相比于 JavaScript 能晋升肯定的性能。

WebAssembly 是一种新的编码方式,能够在古代的网络浏览器中运行 - 它是一种低级的类汇编语言,具备紧凑的二进制格局,能够靠近原生的性能运行,并为诸如 C / C ++ 等语言提供一个编译指标,以便它们能够在 Web 上运行。它也被设计为能够与 JavaScript 共存,容许两者一起工作。

—— MDN

OK,上面就依照以下步骤,进行一一验证下面的思路是否可行

  • 编写 Go kriging 算法代码与性能测试和剖析
  • 利用 WebAssembly 编译 Go 代码与浏览器环境性能测试和剖析
  • 测试性能比照后果
  • 总结一下

2 编写 Go kriging 算法代码与性能测试和剖析

2.1 编写 kriging 代码

一般克里金的模型函数(半变异函)三个模型函数代码

// krigingVariogramGaussian gaussian variogram models
func krigingVariogramGaussian(h, nugget, range_, sill, A float64) float64 {return nugget + ((sill-nugget)/range_)*
        (1.0-math.Exp(-(1.0/A)*math.Pow(h/range_, 2)))
}

// krigingVariogramExponential exponential variogram models
func krigingVariogramExponential(h, nugget, range_, sill, A float64) float64 {return nugget + ((sill-nugget)/range_)*
        (1.0-math.Exp(-(1.0/A)*(h/range_)))
}

// krigingVariogramSpherical spherical variogram models
func krigingVariogramSpherical(h, nugget, range_, sill, A float64) float64 {
    if h > range_ {return nugget + (sill-nugget)/range_
    } else {return nugget + ((sill-nugget)/range_)*
            (1.5*(h/range_)-0.5*math.Pow(h/range_, 3))
    }
}

依据训练模型预测数据生成插值数据代码

// Predict model prediction
func (variogram *Variogram) Predict(x, y float64) float64 {k := make([]float64, variogram.N)
    for i := 0; i < variogram.N; i++ {k[i] = variogram.model(
            math.Pow(math.Pow(x-variogram.x[i], 2)+math.Pow(y-variogram.y[i], 2),
                0.5,
            ),
            variogram.Nugget, variogram.Range,
            variogram.Sill, variogram.A,
        )
    }

    return krigingMatrixMultiply(k, variogram.M, 1, variogram.N, 1)[0]
}

代码较多这里只贴出三个模型函数与预测数据代码,更多代码查看 go-kriging/blob/main/ordinarykriging/ordinarykriging.go

2.2 测试 Golang 代码

ordinaryKriging := ordinary.NewOrdinary(data["values"], data["lons"], data["lats"])
// 训练模型
ordinaryKriging.Train(ordinary.Exponential, 0, 100)
// 生成插值后的矩阵数据
gridMatrices := ordinaryKriging.Grid(polygon, 0.01)
// ...

2.2.1 调试剖析代码耗时

应用 go tool pprof 性能监控与剖析 Go 程序,这里次要调试 CPU 耗时剖析,这里 Memory 剖析不再开展,main 函数加上了上面几行,跑一下代码,生成 cpu_profile 文件。

import "runtime/pprof"

func main() {cpuProfile, _ := os.Create("cpu_profile")
  pprof.StartCPUProfile(cpuProfile)
  defer pprof.StopCPUProfile()
  // ...
}

输出 go tool pprof cpu_profile 命令调试剖析方才生成的 cpu_profile 文件,查看代码 CPU 执行的耗时状况,显示后果如下

$ go tool pprof cpu_profile
Type: cpu
Time: Dec 5, 2020 at 5:42am (CST)
Duration: 1.72mins, Total samples = 1.57mins (91.64%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) 

程序执行工夫 1.72mins,这也太夸大了,比 JS 都跑的慢?????

输出 top 命令列出 CPU 占比前十个最高的一些运行结点

(pprof) top
Showing nodes accounting for 80.97s, 85.80% of 94.37s total
Dropped 127 nodes (cum <= 0.47s)
Showing top 10 nodes out of 39
     flat  flat%   sum%        cum   cum%
   25.83s 27.37% 27.37%     29.11s 30.85%  github.com/liuvigongzuoshi/go-kriging/internal/ordinary.matrixSolve
   15.81s 16.75% 44.12%     15.81s 16.75%  math.Exp
   15.10s 16.00% 60.13%     30.03s 31.82%  math.pow
    6.01s  6.37% 66.49%     21.85s 23.15%  github.com/liuvigongzuoshi/go-kriging/internal/ordinary.krigingVariogramExponential
    4.70s  4.98% 71.47%     59.02s 62.54%  github.com/liuvigongzuoshi/go-kriging/internal/ordinary.(*Variogram).Predict
    3.62s  3.84% 75.31%      3.73s  3.95%  math.modf
    3.58s  3.79% 79.10%      5.30s  5.62%  math.ldexp
    2.22s  2.35% 81.46%      2.22s  2.35%  runtime.nanotime1
    2.09s  2.21% 83.67%      3.19s  3.38%  math.frexp
    2.01s  2.13% 85.80%      2.01s  2.13%  math.IsInf (partial-inline)
(pprof) 

matrixSolve 这个办法进行了大量的矩阵运算耗时比拟长在情理之中,出乎意料的是 math.Exp、math.pow 等规范库的数学方法耗时也较长,为了确认并查看程序执行全副过程,输出 png 查看输入报告

Predict 函数用到幂运算 math.powmath.Exp 办法耗时就高达 15s ????

2.2.2 尝试解决 math.Exp、math.pow 函数耗时长的问题

咋个看剖析都是 math.Exp、math.pow 这两个包比拟耗时,幂运算函数 func Pow(x, y float64) float64 参数是浮点数据类型,查看源码发现在计算 xy 次方计算过程中,须要做一些非凡解决,比较复杂,首先须要对 x、y 的值做非凡判断,是否等于 0 与 +-1 及正数的状况做非凡解决,前面浮点数的 x**y 运算更是简单。

一路 Google 查问相干 Golang 内容无果,看到一篇 Performance of Various Python Exponentiation Methods Python 外面幂运算测试性能的文章,外面提到作者最近在写一个算法来解决一个编码难题,这个问题波及到在笛卡尔立体上找到一个与所有其余点的间隔最小的点,依据勾股定理间隔可用函数能够用表达式 math.sqrt(dx ** 2 + dy ** 2)。它们能够有几种不同的写法:dx**2math.pow(dx,2)dx*dx,有意思的是它们的性能都不雷同,以下是测试后果:

表达式 计时(10 万次迭代)
x * x 3.87 ms
x ** 2 80.97 ms
math.pow(x, 2) 83.60 ms

最初提到,当幂次超过 15 以及超过 1000 越来越大的时候,math.pow() 与 x * x 运行速度也就越来越靠近了,文章最初总结 JavaScript 也有这种状况,难到 Go 也有这种状况?????

验证问题,批改 Predict 函数,调整 math.pow(x, 2) x*x

// Predict model prediction
func (variogram *Variogram) Predict(x, y float64) float64 {k := make([]float64, variogram.N)
    for i := 0; i < variogram.N; i++ {-        k[i] = variogram.model(
-            math.Pow(-                math.Pow(x-variogram.x[i], 2)+math.Pow(y-variogram.y[i], 2),
-                0.5,
-            ),
-            variogram.Nugget, variogram.Range,
-            variogram.Sill, variogram.A,
-        )
+   x_ := x - variogram.x[i]
+        y_ := y - variogram.y[i]
+        h := math.Sqrt(x*x) + y_*y))
+        k[i] = variogram.model(
+            h,
+            variogram.Nugget, variogram.Range,
+            variogram.Sill, variogram.A,
        )
    }

    return krigingMatrixMultiply(k, variogram.M, 1, variogram.N, 1)[0]
}

同理批改三个模型函数,对于 math.Exp(x) 的耗时解决咱们先做一个简略的判断,依据 e^0 等于 1,同理 x 等于 01 间接返回咱们写好的常量。

// krigingVariogramGaussian gaussian variogram models
func krigingVariogramGaussian(h, nugget, range_, sill, A float64) float64 {+    x := -(1.0 / A) * ((h / range_) * (h / range_))
     return nugget + ((sill-nugget)/range_)*
-        (1.0-math.Exp(-(1.0/A)*math.Pow(h/range_, 2)))
+        (1.0-exp(x))
 }
 
// krigingVariogramExponential exponential variogram models
func krigingVariogramExponential(h, nugget, range_, sill, A float64) float64 {+    x := -(1.0 / A) * (h / range_)
     return nugget + ((sill-nugget)/range_)*
-        (1.0-math.Exp(-(1.0/A)*(h/range_)))
+        (1.0-exp(x))
 }

// krigingVariogramSpherical spherical variogram models
func krigingVariogramSpherical(h, nugget, range_, sill, A float64) float64 {
    if h > range_ {return nugget + (sill-nugget)/range_
    } else {
+        x := h / range_
         return nugget + ((sill-nugget)/range_)*
-            (1.5*(h/range_)-0.5*math.Pow(h/range_, 3))
+            (1.5*(x)-0.5*(x*x*x))
     }
}

写改完代码中相似问题后再跑一次,这次程序耗时如下图所示

不错,???? Interesting!程序运行耗时间接缩短 48.6%Predict 函数从 59.0s 缩短到 17.11s ,math.Exp(x) 从 15.81s 缩短到 9.45s。

剩下来比拟耗时的函数就是 matrixSolvemath.Exp(x)math.Exp(x) 这里除了上述的特值判断外目前暂未找到其它优化办法,上面来尝试解决matrixSolve 函数耗时长的问题。

2.3.4 尝试优化 matrixSolve 函数

matrixSolve 函数次要作用是通过高斯 - 若尔当消元法进行求矩阵的逆,这里求矩阵的逆的次要算法有高斯消元法、LU 合成法,除此之外据说还有 SVD 合成法与 QR 合成法。高斯消元法的算法有高斯消元法、列选主元的高斯消元、全选主元的高斯消元法、高斯 - 若尔当消元法,这里用到了高斯 - 若尔当消元法,工夫复杂度也是 O(n^3),占程序总耗时 27-29s 左右。

高斯 - 若尔当消元法 (Gauss-Jordan Elimination) 是高斯消元法的另一个版本,相比起高斯消元法,这个算法的效率比拟低,却可把方程组的解用矩阵一次过示意进去。

—— 维基百科

既然高斯 - 若尔当消元法算法效率比拟低又有没有其它可替换的算法呢?在下面几种算法比照理解各种的优缺点及适应的利用场景后,进行测试发现列主元消去法要快一点,然而还是不够快,最初抉择业余的数学科学计算 Gonum 包进行求矩阵的逆, Gonum 里对矩阵的逆运算用到了并发运算,最初 matrixSolve 函数只占主协程耗时 1-2s 左右,但应用这个办法(猜想外部用的 LU 合成法)进行求矩阵的逆,与之前的高斯 - 若尔当消元法相比,最终的插值的数据有有偏差,偏差位在小数点 12 位,在承受范畴内,可忽略不计,如下是调用数学科学计算 Gonum 包的 Inverse 进行求矩阵的逆代码

func matrixInverse(x []float64, n int) ([]float64, bool) {a := mat.NewDense(n, n, x)
    var ia mat.Dense

    err := ia.Inverse(a)
    if err != nil {return x, false}

    return ia.RawMatrix().Data, true}

对于求矩阵的逆相干有品质的内容:

  • 对于高斯消元思维及实现过程可看看消元法及高斯消元法思维
  • 不同的高斯消元法的性能比拟可看看这个仓库 github.com/ecjtuliwei/GaussianElimination

2.3.5 施展 Go 协程的魅力

代码的性能绝对比较简单,所以比拟容易的定位到了问题的所在,如果还要想进行调优,能够思考进行并发革新,来施展 Go 协程的魅力。

Train 函数外部算法数据计算关联性比拟强,感觉不适宜做并发革新,但调用 Predict 函数的 Grid 函数有做反复事件的滋味,这里 Grid 函数次要作用依据面数据插值生成网格单元集数据,能够将屡次遍历循环生成网格单元数据集的反复逻辑单位散发为多个协程去做这个事件,接下来批改代码,上面是 Grid 函数外部革新成并产生成插值数据的次要代码

// ...
var wg sync.WaitGroup
predictCh := make(chan *PredictDate, (b[1]-b[0])*(a[1]-a[0]))
var parallelPredict = func(j, k int, polygon []Point, xTarget, yTarget float64) {predictDate := &PredictDate{X: j, Y: k}
  predictDate.Value = variogram.Predict(xTarget,
                                        yTarget,
                                       )
  predictCh <- predictDate
  defer wg.Done()}

var xTarget, yTarget float64
for j := a[0]; j <= a[1]; j++ {xTarget = xlim[0] + float64(j)*width
  for k := b[0]; k <= b[1]; k++ {yTarget = ylim[0] + float64(k)*width

    if pipFloat64(currentPolygon, xTarget, yTarget) {wg.Add(1)
      go parallelPredict(j, k, currentPolygon, xTarget, yTarget)
    }
  }
}

go func() {wg.Wait()
  close(predictCh)
}()

for predictDate := range predictCh {
  if predictDate.Value != 0 {
    j := predictDate.X
    k := predictDate.Y
    A[j][k] = predictDate.Value
  }
}
// ...

在尝试并发革新后发现 Grid 函数的执行工夫从原来的 18s 多升高到 4s 多,不错,还行????。

这里多提一下,如果在尝试并发革新后发现革新的后果并不现实,可能是因为应用 channel 进行同步导致阻塞,对消了多协程带来的性能晋升,这种状况就弊大于利了。

通过下面这些优化代码过后,下图是最终优化过后的 CPU profile 剖析部分图

通过基准测试,2000 条数据插值生成图片继续运行工夫大抵 4-6s 左右????,不错。这里就不开展赘述将这个工具做成 REST 服务了,OK,能够开始下一步 Go WebAssembly 了。

3 利用 WebAssembly 技术编译 Go 代码

3.1 编写 Go 代码给 Js 调用办法

主函数办法如下,利用一个通道,让程序始终运行

func main() {fmt.Println("Instantiate, kriging WebAssembly! v0.0.5")
    done := make(chan int, 0)
    js.Global().Set("OrdinaryKriging", js.FuncOf(OrdinaryKrigingFunc))
    js.Global().Set("OrdinaryKrigingTrain", js.FuncOf(OrdinaryKrigingTrainFunc))
    <-done
}

实现训练模型办法被 JS 调用,代码如下

func OrdinaryKrigingTrainFunc(this js.Value, args []js.Value) interface{} {values := make([]float64, args[0].Length())
    for i := 0; i < len(values); i++ {values[i] = args[0].Index(i).Float()}
    lons := make([]float64, args[1].Length())
    for i := 0; i < len(lons); i++ {lons[i] = args[1].Index(i).Float()}
    lats := make([]float64, args[2].Length())
    for i := 0; i < len(lats); i++ {lats[i] = args[2].Index(i).Float()}
    model := args[3].String()
    sigma2 := args[4].Float()
    alpha := args[5].Float()

    variogram := RunOrdinaryKrigingTrain(values, lons, lats, model, sigma2, alpha)
    variogramBuffer, err := json.Marshal(variogram)
    if err != nil {log.Fatal(err)
    }

    return string(variogramBuffer)
}

func RunOrdinaryKrigingTrain(values, lons, lats []float64, model string, sigma2 float64, alpha float64) *ordinarykriging.Variogram {ordinaryKriging := ordinarykriging.NewOrdinary(values, lons, lats)
    variogram := ordinaryKriging.Train(ordinarykriging.ModelType(model), sigma2, alpha)
    return variogram
}

更多代码查看 go kriging wasm examples。

3.2 将 Golang 代码编译成 Wasm 文件

 GOOS=js GOARCH=wasm go build -o kriging.wasm

运行下面的命令生成的 wasm 文件都在 3M 以上,官网倡议有两种计划,一种通过压缩算法工具进行压缩,另一种应用 TinyGo 工具编译生成 Wasm 文件来替换 go build

测试了一下,TinyGo 工具编译生成 Wasm 文件是小了很多,只有 392 kb,如果还感觉大还能够应用压缩工具进行压缩,不过遗憾的是 TinyGo 目前还不反对多协程 Golang 代码编译成 Wasm 文件。

3.3 JavaScript 调用 WebAssembly 次要代码

$(go env GOROOT)/misc/wasm 目录下拷贝引入 wasm_exec.js 文件

<html>
    <head>
        <meta charset="utf-8"/>
        <script src="wasm_exec.js"></script>
        <script>
            const go = new Go();
            WebAssembly.instantiateStreaming(fetch("kriging.wasm"), go.importObject).then((result) => {go.run(result.instance);
            });
        </script>
    </head>
    <body></body>
</html>

批改 WebAssembly 初始化办法,并增加调用办法,更多代码查看 kriging wasm example。

const run = async function (fileUrl) {
  try {const file = await fetch(fileUrl);
    const buffer = await file.arrayBuffer();
    const go = new Go();
    const {instance} = await WebAssembly.instantiate(buffer, go.importObject);
    go.run(instance);

    console.time("训练模型耗时");
    const variogram = RunOrdinaryKrigingTrain(
      t,
      x,
      y,
      params.krigingModel,
      params.krigingSigma2,
      params.krigingAlpha,
      params.krigingWidth
    );
    console.timeEnd("训练模型耗时");
    console.log("variogramResult:", JSON.parse(variogram));

    console.time("训练模型加插值总耗时");
    const gridrResult = RunOrdinaryKriging(
      t,
      x,
      y,
      params.krigingModel,
      params.krigingSigma2,
      params.krigingAlpha,
      params.krigingWidth,
      JSON.stringify(geometry)
    );
    console.timeEnd("训练模型加插值总耗时");
    console.log("gridrResult:", JSON.parse(gridrResult));
  } catch (err) {console.error(err);
  }
};
setTimeout(() => run("./kriging.wasm"));

如果想理解更多对于 Go WASM 查看 Go WASM Wiki

4 测试比照效率

测试设施 MBP CPU 2.6 GHz 六核 Intel Core i7,测试数据 2000+ 条数据,Golang version 1.15.5,Chrome 87,Firefox 83,Kriging 算法函数模型为 spherical (球面半变异函数模型)

类型 JS 版 Chrome 下 Golang 版 Golang 协程并发版 Golang 版编译的 Wams Golang 协程并发版编译的 Wams
训练模型 60-62s 2s 2s 44-50s(Chrome)/130-132s(Firefox) 44-50s(Chrome)
生成插值矩阵数据 59-60s 9-10s 2-4s
总耗时 120-122s 10-12s 4-6s 103-106s(Chrome)/61-285s(Firefox) 122-129s(Chrome)/ 呈现谬误(Firefox)

从下面能够看出,Golang 协程并发版性能是最好的,但 Wams 测试进去的后果就是有点费解了????。

首先 Chrome 下 Golang 应用协程版编译的 Wams 总体性能倒是跟 JS 版差不多,这就有点迷糊了,从训练模型的耗时来看是要比 JS 代码性能好些,然而生成插值就慢了很多,通过屡次测试 Golang 代码编译的 Wams 在浏览器下运行,发现有内存泄露景象,尝试应用 TinyGo 编译生成 Wams 通过测试成果也不是很现实。

这里 Golang 应用协程版编译的 Wams,如果浏览器反对 WebAssembly 多线程,那么就会启用多线程,Chrome 70 当前曾经反对 WebAssembly 多线程了。

在 Firefox 下测试 Wams,如果是协程版编译的 Wams 间接就报错了,未应用协程版的耗时比 JS 版的都高,单看训练模型耗时就十分高,排查了一下,发现训练模型函数返回的数据量很大,大略有 200M+,猜想应该是从外面拷数据到 JS 内存的过程中太耗时了,然而通过测试在不返回模型数据的状况下仍旧还是这么耗时,编译的同一套代码在 Firefox 与 Chrome 状况各不一样????

须要提一下的是下面 JS 版测试未进行 Go 代码那样优化,应用的是 kriging.js 这个包间接进行的测试,如果优化的话激进预计应该可晋升 10-20s 左右吧,在 JS 外面将算法改成并发版不太容易,只能在 Web Workers 外面再创立 Web Workers 线程,没有通过测试还不确定具体成果怎么样。

5 总结

目前 WebAssembly 还不反对调试,只能通过控制台打印相干信息,遇到麻烦很难找到问题出在哪里,不晓得是 Golang 编译 Wams 对多过程反对成熟度不够还是头大的 GC 问题。前面有可能的话试一下同样的算法用 Emscripten 编译 C/C++ 的 Wams 看看状况如何。

折腾了这么多还是倡议将 kriging 插值算法做成服务,部署到 CPU 比拟好的服务器上,其次服务器上最好做一个缓存性能,同样的数据输出就不须要再花工夫插值计算一次了。

Golang Kriging 算法包还有持续优化欠缺的中央,特地是设计到的矩阵运算代码,目前还没有笼罩残缺的测试以及 CLI 与应用文档,前面会慢慢增加上,仓库地址:

  • go-kriging – Golang Kriging 算法代码基于 kriging.js 重写的,代码与算法上做了优化,并增加了一些新办法。
  • kriging-wasm example – go-kriging 算法代码编译的 Wasm 应用状况及测试示例。
  • go-kriging-service – 调用 go-kriging 算法包编写的 HTTP 服务,反对多用户并发调用,有简略的日志记录与容错复原机制性能。

后话:kriging 算法耗时次要是矩阵运算这块,假使利用图形处理器 (GPU) 减速计算这样的算法与基于 CPU 的算法相比拟,GPU 减速计算是否获得更快的运算速度呢?

参考资料

  • ordinary-kriging – 一般克里金法在线工具
  • 克里金法 (3D Analyst) – ArcGIS
  • 克里金法的工作原理 – ArcGIS
  • 克里金法 – 百科词条
  • Kriging – wikipedia 克里金法
  • Multivariate_interpolation – wikipedia 多元插值
  • kriging.js – Javascript library for geospatial prediction and mapping via ordinary kriging
  • WebAssembly Threads ready to try in Chrome 70
  • c++ – 在浏览器中,多线程 WebAssembly 的速度比单线程慢,为什么?
  • WebAssembly Interface Types: Interoperate with All the Things!
  • Go, WebAssembly, HTTP requests and Promises
  • golang — 性能测试和剖析

文中链接较多倡议原文地址查阅。

正文完
 0