首页 > 其他分享 >通过解析库探究函数式抽象代价

通过解析库探究函数式抽象代价

时间:2024-02-13 22:00:15浏览次数:27  
标签:解析器 ns 16 HexColor 探究 抽象 str static 解析

前因

在春节前了解到 Rust语言有一个叫 nom 的解析库

它可以让你创建安全的解析器,而不会占用内存或影响性能。

它依靠 Rust 强大的类型系统和内存安全来生成既正确又高效的解析器,并使用函数,宏和特征来抽象出容易出错的管道。

nom 核心是解析器组合器,而解析器组合器是高阶函数,可以接受多个解析器作为输入,并返回一个新的解析器作为输出。

这种方式让你可以为简单的任务(如:解析某个字符串或数字)构建解析器,并使用组合器函数将它们组合成一个递归下降(recursive descent)的解析器。

组合解析的好处包括可测试性,可维护性和可读性。每个部件都非常小且具有自我隔离性,从而使整个解析器由模块化组件构成。

由此萌生在其他非函数语言探究函数抽象代价的想法,如何优雅语义化代码前提下追求极致的性能应该是一个非常复杂的问题

所以在春节期间选择了熟悉的 c# 语言(避免对语言的不熟悉导致无法充分利用性能)进行了尝试

调研

春节一开始,为了解除这个让我寝食难安的想法,没有片刻的休息,便开始了纠结反复长期的探究。

当然解析库的顶流必然是ANTLR,其完全避免了大家在词法语法解析这项费事费力极为复杂的事情上浪费时间

当然我研究ANTLR的话就完全偏离之前预定的函数抽象代价的想法

那么 c# 语言有没有过其他人有过类似用函数思想进行解析器简化抽象的想法呢?

一番查找,发现还真有人在多年前就做尝试,代表者为Sprache

其不完全是典型函数组合思想,而是c#语言中Linq这数据处理函数库以及其类sql表达结合形式

以HexColor解析举例说明

HexColor 为16进制颜色值,其以 "#" 开头, 接着6个16进制数(0-9和a-f,大小写不敏感), 每2个值构成一组, 从左到右为 RGB 通道值, 可以简写为 #R(hex, hex)G(hex, hex)B(hex, hex).

所以解析器逻辑可以概括为:去掉开头的 "#", 如果随后的6个字符为16进制数, 则分为三组, 并将每组的值从16进制转换为10进制.

手写HexColor解析器

如果手写代码,该解析器代码如下:

private static (byte red, byte green, byte blue) HexColorHande(string str)
{
    if (str.Length == 7 && str[0] is '#')
    {
        for (var i = 1; i < str.Length; i++)
        {
            if (!char.IsAsciiHexDigit(str[i]))
            {
                throw new FormatException("Must has 6 AsciiHexDigit");
            }
        }
        return (Convert.ToByte(str[1..3], 16), Convert.ToByte(str[3..5], 16), Convert.ToByte(str[5..7], 16));
    }
    throw new ArgumentException("No perfix with #");
}

除了Range运算符产生子串之外,没有多余分配和判断消耗,以此作为性能基线,应该可以明显看出其他实现产生的代价

Sprache 的HexColor解析器

public static class SpracheHexColorTest
{
    /// 通过字段缓存函数,避免多次构建
    private static Sprache.Parser<(byte red, byte green, byte blue)> identifier;

    static SpracheHexColorTest()
    {
        identifier =
        from leading in Sprache.Parse.Char('#').Once()
        from s in Sprache.Parse.LetterOrDigit.Repeat(6).Text()
        select (Convert.ToByte(s[0..2], 16), Convert.ToByte(s[2..4], 16), Convert.ToByte(s[4..6], 16));
    }

    public static (byte red, byte green, byte blue) Parse(string content) => identifier.Parse(content);
}

可以看到代码还是非常语义化,虽然个人觉得类sql形式还是有点不习惯,但其实也是对于Linq过长链条代码语义复杂问题的比较好的解决形式

那么性能对比结果会是怎么样呢?

Method Mean Error StdDev Gen0 Gen1 Allocated
Hande_HexColor 59.04 ns 0.484 ns 0.429 ns 0.0153 - 96 B
Superpower_HexColor 475.21 ns 3.690 ns 3.081 ns 0.1268 - 800 B
Sprache_HexColor 621.27 ns 4.610 ns 4.312 ns 0.4692 0.0010 2944 B

ps: Superpower 号称是 Sprache 的性能优化版本,两者核心思路一致,只是一些实现细节有所差别,两者使用形式一致,这里就不展示其代码了

可以看到有着比较明显的差别,当然在一般项目中,这点消耗完全没必要考虑(但我的目的本身就是吹毛求疵,这些差异肯定是要探究的)

经过简单查阅代码,其主要有以下差异:

  • 函数实际是以delegate作为基石

    所以对于手写的解析器肯定有更多的delegate调用以及次数代价,不过这点由于语言特性,如果依旧要函数优化语义,多半没有其他优化选择了

  • 函数不支持多返回值

    所以封装了Result结构体,以此包含更多所需的结果,当然也带来了更多的分配消耗

  • IEnumerable<T>作为Sprache另外一个基石

    在这里实际导致无法使用原始的string,毕竟string的还是一个比较复杂的对象

优先尝试减少多返回值

通过 out 避免Result结构体

public delegate int Is<T>(IPeeker<T> input, out T t);

// 一些函数实现举例
public static Is<char> Is(char c) => Parser.Is<char>(i => i == c, 1);

public static Is<T> Is<T>(Func<T, bool> predicate, int count) => (IPeeker<T> i, out T t) =>
{
    if (i.TryPeek(out t) && predicate(t))
    {
        return count;
    }
    return 0;
};

public static Func<IPeeker<T>, T> Once<T>(this Is<T> predicate, Func<IPeeker<T>, Exception> ex) => i =>
{
    var c = predicate(i, out var t);
    if (c <= 0)
    {
        throw ex(i);
    }
    i.Read(c);
    return t;
};

// HexColor解析器
public static class HexColorOnlyChar
{
    private static readonly Func<IPeeker<char>, char> tag_Start = Chars.Is('#').Once("# is Required.");

    private static Func<IPeeker<char>, char[]> HexDigit = Chars.IsAsciiHexDigit.Repeat(6, "Must has 6 AsciiHexDigit");

    public static (byte red, byte green, byte blue) Parse(string str)
    {
        var input = Input.From(str);
        tag_Start(input);
        var s = new string(HexDigit(input));
        var r = (Convert.ToByte(s[0..2], 16), Convert.ToByte(s[2..4], 16), Convert.ToByte(s[4..6], 16));
        if (input.TryPeek(out var c))
        {
            throw new FormatException(c.ToString());
        }
        return r;
    }
}

ps: 这里实际略掉了一些 错误信息 以及行列计算的信息,为了简单,暂时不讨论,忽略其影响,重心先看与基线的差异

Method Mean Error StdDev Gen0 Gen1 Allocated
Hande_HexColor 59.04 ns 0.484 ns 0.429 ns 0.0153 - 96 B
RuQu_HexColorOnlyChar 170.10 ns 2.317 ns 2.168 ns 0.0572 - 360 B
Sprache_HexColor 621.27 ns 4.610 ns 4.312 ns 0.4692 0.0010 2944 B

可以看到,减少了非常多处理逻辑后,性能好了很多,我们再做更多尝试

最终实现

public static class HexColor
{
    /// Take<char> 实际调用 TryPeek(int count, out IPeekSlice<char> data) , 降低 
    private static readonly Func<IPeeker<char>, string> HexDigitString = Parser.Take<char>(6, "Must has 6 AsciiHexDigit").Map(ii =>
    {
        var s = ii.ToString();
        for (var i = 0; i < s.Length; i++)
        {
            if (!char.IsAsciiHexDigit(s[i]))
            {
                throw new FormatException("Must has 6 AsciiHexDigit");
            }
        }
        return s;
    });

    private static readonly Action<IPeeker<char>> NoMore = Chars.Any.NoMore("Only 7 chars");
    private static readonly Func<IPeeker<char>, char> TagStart = Chars.Is('#').Once("# is Required.");

    public static (byte red, byte green, byte blue) Parse(string str)
    {
        var input = Input.From(str);
        TagStart(input);
        var s = HexDigitString(input);
        NoMore(input);
        return (Convert.ToByte(s[0..2], 16), Convert.ToByte(s[2..4], 16), Convert.ToByte(s[4..6], 16));
    }
}

对比上一版核心降低了 IEnumerable<char>new string 代价

/// TryPeek 通过Substring 减少遍历次数
public bool TryPeek(int count, out IPeekSlice<char> data)
{
    var i = readed + count;
    if (i >= str.Length)
    {
        data = null;
        return false;
    }
    data = new PeekString(str.Substring(readed + 1, count));
    return true;
}

/// PeekString 减低了 toArray 和 new string 的消耗
public class PeekString : IPeekSlice<char>
{
    private string str;

    public PeekString(string str)
    {
        this.str = str;
    }

    public override string ToString()
    {
        return str;
    }
}

完整性能测试结果


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3085/23H2/2023Update/SunValley3)
Intel Core i7-9750H CPU 2.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK 8.0.101
  [Host]     : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
  DefaultJob : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2


Method Mean Error StdDev Gen0 Gen1 Allocated
Hande_HexColor 59.04 ns 0.484 ns 0.429 ns 0.0153 - 96 B
RuQu_HexColor 81.76 ns 0.551 ns 0.489 ns 0.0305 - 192 B
RuQu_HexColorOnlyChar 170.10 ns 2.317 ns 2.168 ns 0.0572 - 360 B
Superpower_HexColor 475.21 ns 3.690 ns 3.081 ns 0.1268 - 800 B
Sprache_HexColor 621.27 ns 4.610 ns 4.312 ns 0.4692 0.0010 2944 B
// * Legends *
  Mean      : Arithmetic mean of all measurements
  Error     : Half of 99.9% confidence interval
  StdDev    : Standard deviation of all measurements
  Gen0      : GC Generation 0 collects per 1000 operations
  Gen1      : GC Generation 1 collects per 1000 operations
  Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
  1 ns      : 1 Nanosecond (0.000000001 sec)

可以看到性能比上一版减少了一半,非常接近手写那版,如果后续不存在函数实现的限制,该方式将会是比较接近一行一行优化的手写方式的尝试方案

总结

  • delegate 肯定会有调用消耗,不过单次非常小,只是函数抽象一般量大
  • 抽象可以简化问题,但也容易屏蔽特定场景优化,带来多余消耗
  • 编码还是需要安静环境,不然容易注意力分散而昏头浪费时间,哈哈哈哈

虽然春节花费了不少时间,确定了一些认知,但性能与优雅语义化依然如太极阴阳平衡艰难

完整代码参考 https://github.com/fs7744/ruqu

该代码仅为早期研究,不代表最终成型,也缺乏完整功能,还有待持续开发

后续会以 ini 文件解析探究更多复杂语义函数实现

标签:解析器,ns,16,HexColor,探究,抽象,str,static,解析
From: https://www.cnblogs.com/fs7744/p/18014877

相关文章

  • Torrent文件结构解析
    Torrent文件内的数据结构分为以下几部分:announce:Tracker的主服务器announce-list:Tracker服务器列表comment:种子文件的注释comment.utf-8:种子文件注释的utf-8编码creationdate:种子文件建立的时间,是从1970年1月1日00:00:00到现在的秒数。encoding:种子文件的默认编码,比如GB......
  • Python通过Lxml库解析网络爬虫抓取到的html
    ​Lxml是基于libxml2解析库的Python封装。libxml2是使用C语言编写的,解析速度很好,不过安装起来稍微有点复杂。安装说明可以参考(http://Lxml.de/installation.html),在CentOS7上中文安装说明(http://www.cjavapy.com/article/64/),使用lxml库来解析网络爬虫抓取到的HTML是一种非常......
  • 探索C语言的内存魔法:动态内存管理解析
    ✨✨欢迎大家来到贝蒂大讲堂✨✨......
  • CMake构建Makefile解析
    CMake构建Makefile解析来源 https://zhuanlan.zhihu.com/p/661284197 一、CMake构建后的项目结构解析(AnalysisoftheProjectStructureAfterCMakeBuild)1.1CMake构建后的目录结构(DirectoryStructureAfterCMakeBuild)CMake构建完成后,会在项目的根目录下生成一个名为......
  • Java break、continue 详解与数组深入解析:单维数组和多维数组详细教程
    JavaBreak和ContinueJavaBreak:break语句用于跳出循环或switch语句。在循环中使用break语句可以立即终止循环,并继续执行循环后面的代码。在switch语句中使用break语句可以跳出当前case,并继续执行下一个case。示例://循环示例for(inti=0;i<10;i++......
  • 【c&c++】可变参数:va_list(),va_start(),va_arg(),va_end() 详细解析
    目录1、含义:2、使用:3、连续打印出自定义格式的文字:1、含义:(1)va_list是C语言中的一个宏定义,用于表示一个变长参数列表。它是一个指向变长参数列表的指针,可以通过宏va_start、va_arg和va_end对变长参数列表进行访问和操作。在函数中需要接收不定数量的参数时,可以使用va_list来处......
  • 解析Sermant热插拔能力:服务运行时动态挂载JavaAgent和插件
    本文分享自华为云社区《服务运行时动态挂载JavaAgent和插件——Sermant热插拔能力解析》,作者:华为云高级软件工程师栾文飞一、概述Sermant是基于Java字节码增强技术的无代理服务网格,其利用Java字节码增强技术,为宿主应用程序提供服务治理功能,以解决大规模微服务场景中的服务治理......
  • Go语言的For循环:语法全解析
    Go语言,作为一门旨在提供简洁、高效编程体验的编程语言,其循环结构的设计同样体现了这一理念。在Go中,for循环是唯一的循环语句,但它的灵活性足以应对各种迭代需求。本文将详细介绍Go语言中for循环的语法,通过示例展示其在实际编程中的应用。基本语法Go语言的for循环基本语法如下:for初......
  • Linux常用命令全解析
    Linux是一个强大的操作系统,广泛应用于服务器、云计算、网络设备等领域。熟练使用Linux命令行是每一个IT专业人士必备的技能。本文旨在为大家提供一个Linux常用命令的快速参考指南,包括命令的基本用法、示例以及简短解释,帮助大家提高在Linux环境下的工作效率。文件和目录操作ls-列......
  • Linux常用命令全解析
    Linux是一个强大的操作系统,广泛应用于服务器、云计算、网络设备等领域。熟练使用Linux命令行是每一个IT专业人士必备的技能。本文旨在为大家提供一个Linux常用命令的快速参考指南,包括命令的基本用法、示例以及简短解释,帮助大家提高在Linux环境下的工作效率。文件和目录操作ls-列......