F#的尾递归编译优化需要再好好优化优化

2023-10-31

先来看一道简单的算法题:

给定一个整数序列,给定一个目标值,求出该序列中任意三个数之和中最接近目标值的那个数。

这道题很容易想到的算法:

  1. 对序列做从小到大排序
  2. 固定其中一个数的下标a,对剩下的两个数双指针b、c,指向a右侧区域(窗口)的两端。根据a、b、c三处值之和与目标值大小关系,窗口不断向内收缩两端的b、c指针,直到找到三者之和等于目标值,或者窗口两端指针重合,此时得到a的当前取值下的较优解。
  3. 从左到右遍历a,找到所有a取值的较优解,并选出最优解。

考虑上述算法用F#实现,首先比较容易思考的是使用变量。(毕竟我们跟变量打了许多年交道了):

let threeSumClosest nums target =
    let sorted = Array.sort nums
    let mutable diff = System.Int32.MaxValue
    let mutable rsl = target
    for a in 0 .. sorted.Length - 2 do
        let mutable b = a + 1
        let mutable c = sorted.Length - 1
        while (diff <> 0 && b < c) do
            let sum = sorted[a] + sorted[b] + sorted[c]
            let rest = abs (sum - target)
            if rest < diff then 
                diff <- rest
                rsl <- sum
            if sum < target then b <- b + 1 
            elif sum > target then c <- c - 1
    rsl

很容易理解对吧。

然而,这代码显然不够“函数”。真正的函数式编程应该不存在变量。咱们尝试下使用纯函数来实现。

用了变量,for、while循环可以随便用。但是如果使用纯函数,while循环基本就再见了(while返回的是unit),for基本只能用来返回序列了。而循环大部分时间会使用递归描述。

此处递归函数定义:对于每层递归,根据当前确定选定的a、b、c,返回a固定、b和c取值在当前b、c值之间时,与目标最小差距的和。

于是,可以写出如下实现:

let threeSumClosest2 nums target =
    let inline min target sum x = if abs (sum - target) < abs (x - target) then sum else x
    let rec inner (nums: int[]) target a b c =
        let sum = nums[a] + nums[b] + nums[c]
        if b >= c then System.Int32.MaxValue
        elif sum = target then sum
        elif sum < target then min target sum <| inner nums target a (b + 1) c
        else min target sum <| inner nums target a b (c - 1)
    let sorted = Array.sort nums
    seq { for i in 0 .. sorted.Length - 2 -> inner sorted target i (i + 1) (sorted.Length - 1) } 
    |> Seq.minBy (fun x -> abs (x - target))

这里已经没有变量了。

然而,这里使用了递归,在数据量上来之后,反复递归会导致性能的下降。

我们考虑将代码优化一下,变成尾递归。F#的编译器会将尾递归自动编译为循环。

尾递归的要诀是:不保存任何中间值。也就是说,递归会一直进行到最后一层,并由最后一层递归的返回作为所有递归层的返回结果。

于是,我们将每次计算的三者之和与之前计算的较优的和进行比较,把更接近目标的值作为中间的较优结果,传递给下一层递归。

优化后代码如下:

let threeSumClosest3 nums target =
    let inline min target sum x = if abs (sum - target) < abs (x - target) then sum else x
    let rec inner (nums: int[]) target almost a b c =
        if a > nums.Length - 2 then almost
        elif b >= c then inner nums target almost (a + 1) (a + 2) (nums.Length - 1)
        else
            let sum = nums[a] + nums[b] + nums[c]
            if sum = target then sum
            else
                let better = min target sum almost
                if sum < target then inner nums target better a (b + 1) c
                else inner nums target better a b (c - 1)
    let sorted = Array.sort nums
    inner sorted target (System.Int32.MaxValue) 0 1 (sorted.Length - 1)

这里的每层递归不在保存任何中间值了。较优值会一路传递到最后一次递归,最后一路返回。

OK,终于要进入本文的重点了。

我们需要验证下代码是不是被编译成了循环,而不是递归调用。

我们查看下编译后的IL代码,将他反编译成C#代码,于是看到了惊掉下巴的奇葩代码:

internal static int inner_00404(int[] nums, int target, int almost, int a, int b, int c)
{
    int num5;
    while (true)
    {
        if (a > nums.Length - 2)
        {
            return almost;
        }

        if (b >= c)
        {
            int[] array = nums;
            int num = target;
            int num2 = almost;
            int num3 = a + 1;
            int num4 = a + 2;
            c = nums.Length - 1;
            b = num4;
            a = num3;
            almost = num2;
            target = num;
            nums = array;
            continue;
        }

        num5 = nums[a] + nums[b] + nums[c];
        if (num5 == target)
        {
            break;
        }

        int num6 = (Math.Abs(num5 - target) >= Math.Abs(almost - target)) ? almost : num5;
        if (num5 < target)
        {
            int[] array2 = nums;
            int num7 = target;
            int num8 = a;
            int num9 = b + 1;
            c = c;
            b = num9;
            a = num8;
            almost = num6;
            target = num7;
            nums = array2;
        }
        else
        {
            int[] array3 = nums;
            int num10 = target;
            int num11 = a;
            int num12 = b;
            c--;
            b = num12;
            a = num11;
            almost = num6;
            target = num10;
            nums = array3;
        }
    }

    return num5;
}

里面先int numXX = target;,紧接着target=numXX;的代码比比皆是。更可气的是有c=c这样的代码。

不过仔细想一想,又没有什么毛病。作为编译器的自动优化,一定要找到通用的规则才行,这个通用规则可以不够搞笑,关键是要稳。上述翻译要解决的问题是:传递给一下次循环的参数,有可能相互之间有运算关系。 如果不把所有的值先算出来,而是算一个赋值一个,可能计算后续参数值的时候,前面的参数已经变化了,导致计算错误。

从上述翻译我们可以猜测编译器的优化逻辑:

  1. 建立方法,方法传入的参数作为循环体共享的循环变量。建立循环,循环中的出口对应尾递归中的出口。
  2. 按照顺序解析代码,将尾递归的每一个分支放在if中:
    1. 如果该分支没有递归调用,则认为是出口。
      • 通过return返回结果。
      • 由于循环外必须有一个return,所以其中一个会被翻译成break,在循环外return。
      • 又因为这些分支都已经return或者break了,其他分支可以放在主方法体里,而不用放在else里。
    2. 如果该分支有递归调用,则认为需要进入下一层循环。
      • 在主方法体内书写分支逻辑
      • 执行到递归调用时,判断每个参数是否影响到其他参数的变化
        • 不影响其他参数的参数,不需要中间变量,直接进行赋值操作。例如上例中的c。
        • 影响其他参数的参数,先将需要传递给下一次循环的计算结果保存在临时变量中,全部计算完成后再传递给循环共享的变量。

以上是猜测,但是应该八九不离十。可见,尾递归的编译器优化其实并没有那么的智能。感觉还有优化的空间。比如上例中,c=c是完全可以优化掉的。F#还有很长的路要走。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

F#的尾递归编译优化需要再好好优化优化 的相关文章

  • F# 检查列表是否为空

    作为 F 新手 我正在尝试实现一个简单的函数 该函数将索引和列表作为参数 然后返回给定索引的列表值 let rec getElementAtIndex index int list a list match index list with
  • F# 命名约定

    F 是否有 官方 命名 大小写约定 我总是怀疑是否使用 C 风格 Class MyFunctionName or Module my function name 在 F 中 您应该混合 BCL 类和 F 库类 它们具有不同的大小写 并且代码
  • 如何使用 .Net Core 和 VSCode 在调试模式下执行测试?

    如何使用 Net Core 和 VSCode 在调试模式下执行测试 我当前正在命令行上运行以下命令 dotnet Test 但是 这不是在调试模式下执行测试 我要附加调试器吗 如果是这样 怎么办 如有必要 请将测试项目转换为控制台应用程序
  • 如何使 FSI 在 NET5 下工作并让愚蠢的 stackoverflow 消息“标题不能包含...”闭嘴?

    我正在将一个相当小的 F 项目从 Net Framework 迁移到 NET5 迁移非常简单 一切正常 包括测试 但是 当我运行一些脚本时 我现在收到以下错误 Microsoft R F Interactive version 11 0 0
  • 如何解决 FParsec 错误“组合器‘许多’已应用于解析器,该解析器在不消耗...的情况下成功”

    我有一个看起来足够简单的解析器 我将此子解析器添加到末尾以提供有关一般解析错误的信息 因为所有其他子解析器都失败了 Read the rest of a line as an error let readError parse let re
  • F# 中的动态编程

    实现解决问题的动态规划算法的最优雅的方法是什么子问题重叠的问题 http en wikipedia org wiki Overlapping subproblem 在命令式编程中 人们通常会创建一个按问题大小索引的数组 至少在一维 然后算法
  • F# 引用的另一个限制?

    今天早些时候 我遇到了 F 引用的限制 并在这里提出了一个问题 F 引号 变量可能会转义作用域 https stackoverflow com questions 6414185 f quotations variable may esca
  • true 和布尔列表 f# 的长度

    直接使用递归 写一个函数truesAndLength bool list gt int int那 返回列表的长度 在该对的第一个组件中 以及列表的数量 列表中正确的元素 在第二个组件中 你的函数必须只迭代 遍历列表的元素一次 请勿使用 Li
  • 基于函数签名的模式匹配

    在 F 中 您可以对函数签名进行模式匹配 我想用一个函数来装饰多个函数 该函数测量函数的执行情况并调用 statsd 我当前的功能是 let WrapFunctionWithPrefix metrics Metric Client IRec
  • F# 使用 while 循环

    我有一个数据读取器 我想从中返回行集合 在阅读了一天的书籍后 我无法找到在 f 中执行此操作的最佳方法 我可以在 F 中以正常的 C 方式进行操作 但这不是我使用 f 的原因 这就是我想要实现的目标 let values while rea
  • 如何让一条记录实现一个接口?

    如果我有一个界面 type IData abstract member firstName string abstract member lastName string 如何定义符合此接口的记录类型 我尝试了如下所示 gt type Dat
  • 在构建过程中引用自身内部的记录

    我正在尝试创建一条记录 该记录在同一构造函数中使用先前定义的字段之一来计算另一个字段的值 例如 myRecordType Foo int Bar int myRecord Foo 5 Bar Array init Foo fun i gt
  • 图像分析-光纤识别

    我是图像分析新手 您知道如何以仅获取纤维的方式对该图像进行二值化吗 我尝试过不同的阈值技术等 但没有成功 我不介意应该使用什么工具 但我更喜欢 NET or Matlab PS 我不知道该把答案放在哪里 所以我把它放在StackOverfl
  • 什么时候需要使用 new 来初始化 F# 类型?

    给定一个类 例如 type MyClass member this Greet x printfn Hello s x 使用初始化实例是否合适 let x new MyClass 或没有new 另外 什么时候使用new构造函数比 a 更有用
  • 使用反射创建 Action<'T> 的实例

    我将如何创建一个实例Action lt T gt 使用反射 这是我所拥有的 let makeAction typ Type f T gt unit let actionType typedefof
  • F# 方法返回 null 而不是 Option

    我开发F 应用 net 4 6 1 on VS2015 我有方法 type CommonHelper static member SideEffectOnNull act x if x null then act x else x stat
  • 如何在 F# 中组合 Result<> 列表?

    用这个代码 let a 3 4 5 6 7 let check x if x 2 0 then Ok x else Error x let b a gt List map check 我如何将 B 总结为 如果一切OK 则Ok 如果有任何错
  • int -> int list 与类型 int -> IEnumerable<'a> 不兼容

    Given open System Linq 这是一个可以接受的表达方式 2 3 4 SelectMany fun n gt 1 n 但这不是 2 3 4 SelectMany fun n gt 1 n 错误消息显示 int gt int
  • 如何向 F# 项目添加第三方 dll 引用?

    我正在向我的 F 项目添加第三方 dll 引用 我在引用中添加了 dll 当我使用它时 即突出显示代码并执行 Alt Ent 我收到错误 命名空间或模块 AZROLESLib 未定义 我是不是错过了什么 简而言之 你必须使用 r path
  • 如何在 F# 中实现返回 void 的接口成员

    想象一下 C 中的以下接口 interface IFoo void Bar 我如何在 F 中实现这一点 我在 30 分钟的在线搜索中找到的所有示例都仅显示具有返回类型的示例 我认为这在函数式风格中更常见 但在这种情况下我无法避免 这是我到目

随机推荐

  • 网络请求及协议

    TCP IP 协议 图解HTTP常见问题 归类 目录
  • 《clickhouse原理解析与应用实践》读书笔记

    福利置顶 温馨提示 电子版可在微信读书app阅读 第一章 ClickHouse的前世今生 传统BI的局限性 数据仓库 为了解决数据孤岛的问题 即通过引入一个专门用于分析类场景的数据库 将分散的数据统一汇聚到一处 数据仓库的衍生概念 对数据进
  • Docker网络学习

    文章目录 Docker容器网络 1 Docker为什么需要网络管理 2 Docker网络简介 3 常见的网络类型 4 docker 网络管理命令 5 两种网络加入差异 6 网络讲解 docker Bridge 网络 docker Host
  • 腾讯云私有云平台运维面试

    文章目录 概述 JD 岗位描述 一面 二面 三面 HR面 概述 根据会议将面试问题进行总结 很多问题感觉当时没回答好 这是为啥呢 应该还是不熟练吧 或者不善于表达 将次经历分享出来 大家多练练 JD 岗位描述 私有云平台运维 JD 腾讯云智
  • ThreadLocal,看我就够了!

    ThreadLocal 开胃菜 研究过Handler的应该对ThreadLocal比较眼熟的 线程中的Handler对象就是通过ThreadLocal来存放的 初识ThreadLocal的可能被它的名字有所误导 ThreadLocal初一看
  • 将Android项目作为module导入到主项目中

    导入module流程 1 主项目中import需要导入项目的app模块 2 修改该module中build gradle里的com android application为com android library 3 删除该module的ap
  • 【Java】JDBC操作Oracle数据库

    1 Statement 用于执行静态 SQL 语句并返回它所生成结果的对象 statement每次执行sql语句 相关数据库都要执行sql语句的编译 import java sql Connection import java sql Dr
  • 前端例程20220802:玻璃背光按钮

    演示 原理 使用元素包裹按钮 按钮设置为玻璃质感 设置光标悬停动画 使用元素的before和after两个元素作为背景灯光 设置光标悬停动画 代码
  • cmd for命令

    for命令式批处理命令中最复杂也是功能最为强大的一个命令 它可以对一组不同的文件或数据进行循环处理 FOR variable variable IN set DO command command parameters variable 指定
  • pytorch语义分割-全卷积网络

    文章目录 1 语义分割和实例分割 2 语义分割的数据集处理 3 转置卷积 4 全卷积神经网络 FCN 1 语义分割和实例分割 2 语义分割的数据集处理 最重要的语义分割数据集之一是Pascal VOC2012 matplotlib inli
  • linux 可能从硬盘安装吗,从硬盘安装linux(radHat)

    1 gt 从网上下载redhat iso安装文件 并放在同一文件夹中 2 gt 用WinISO解开第一张盘的 iso文件 如解到cd1文件加中 不用全部解出 只要dosutils子文件夹就可以了 3 gt 进入MS DOS打开cd1文件夹的
  • 使用可视化库matplotlib绘图时,plt.show()过后只出现Figure size 640x480 with 1 Axes而没有生成图片

    使用可视化库matplotlib绘图时 plt show 过后只出现
  • Tomcat源码:Acceptor与Poller、PollerEvent

    参考资料 Tomcat源码解析系列 十一 ProtocolHandler Tomcat源码解析系列 十二 NioEndpoint 前文 Tomcat源码 启动类Bootstrap与Catalina的加载 Tomcat源码 容器的生命周期管理
  • 联想计算机连接不上蓝牙耳机,thinkpad如何连接蓝牙耳机_thinkpad连接蓝牙耳机的步骤...

    现在的电脑一般都配备有蓝牙功能 可以方便用户们使用一些蓝牙设备 例如最近就有小伙伴问小编thinkpad如何连接蓝牙耳机 那么针对这一问题 今天小编就来为大家整理分享关于thinkpad连接蓝牙耳机的步骤 一起往下看吧 具体步骤如下 1 先
  • VB封装DLL并调用

    首先明确DLL函数是什么 DLL 动态链接库 Dynamic Link Library 一个DLL文件里面可以包含多个函数 其实就是实现共享函数的一种方式 一个应用程序可能需要多个DLL联合起来才可以正常使用 一 新建ActiveX Dll
  • SpringCloud PK K8s 谁更胜一筹

    SpringCloud PK K8s 谁更胜一筹 Spring Cloud 和 Kubernetes 都声称自己是开发和运行微服务的最佳环境 但它们在本质上有很大的不同 解决的问题也不同 在本文中 我们将看看每个平台是如何交付基于微服务架构
  • 在Eclipse中进行Junit测试的个人总结

    1 怎样在Eclipse中集成使用Junit 想要在Eclipse这个IDE中集成使用Junit 首先需要下载Junit的包 具体下载方式可以自行查阅或翻看我之前有关Junit的博客的前半部分 下载完成后 进入Eclipse 打开工程 左键
  • 转帖:如何注册Filter

    参考文章 http apps hi baidu com share detail 16291532 AX文件的一个对外接口DllRegisterServer 由外部调用 比如注册AX的时候 regsvr32 xxx ax 通常情况下 我们的
  • Web3和 NFT将如何影响电子商务?

    每日更新 欢迎交流 感兴趣可以点个关注 你有没有发现 万维网上有很多改变 并且改变速度还很快 也许你已经读到过青少年将数字资产卖到数百万美元 匿名的加密货币创始人颠覆了传统的金钱概念 那么 这些新的 令人兴奋的 而且通常是奇怪的东西到底是关
  • F#的尾递归编译优化需要再好好优化优化

    先来看一道简单的算法题 给定一个整数序列 给定一个目标值 求出该序列中任意三个数之和中最接近目标值的那个数 这道题很容易想到的算法 对序列做从小到大排序 固定其中一个数的下标a 对剩下的两个数双指针b c 指向a右侧区域 窗口 的两端 根据