.NET性能优化-使用ValueStringBuilder拼接字符串

前言

这一次要和大家分享的一个Tips是在字符串拼接场景使用的,我们经常会遇到有很多短小的字符串需要拼接的场景,在这种场景下及其的不推荐使用String.Concat也就是使用+=运算符。 目前来说官方最推荐的方案就是使用StringBuilder来构建这些字符串,那么有什么更快内存占用更低的方式吗?那就是今天要和大家介绍的ValueStringBuilder

ValueStringBuilder

ValueStringBuilder不是一个公开的API,但是它被大量用于.NET的基础类库中,由于它是值类型的,所以它本身不会在堆上分配,不会有GC的压力。 微软提供的ValueStringBuilder有两种使用方式,一种是自己已经有了一块内存空间可供字符串构建使用。这意味着你可以使用栈空间,也可以使用堆空间甚至非托管堆的空间,这对于GC来说是非常友好的,在高并发情况下能大大降低GC压力。

// 构造函数:传入一个Span的Buffer数组
public ValueStringBuilder(Span<char> initialBuffer);// 使用方式:
// 栈空间
var vsb = new ValueStringBuilder(stackalloc char[512]);
// 普通数租
var vsb = new ValueStringBuilder(new char[512]);
// 使用非托管堆
var length = 512;
var ptr = NativeMemory.Alloc((nuint)(512 * Unsafe.SizeOf<char>()));
var span = new Span<char>(ptr, length);
var vsb = new ValueStringBuilder(span);
.....
NativeMemory.Free(ptr); // 非托管堆用完一定要Free

另外一种方式是指定一个容量,它会从默认的ArrayPoolchar对象池中获取缓冲空间,因为使用的是对象池,所以对于GC来说也是比较友好的,千万需要注意,池中的对象一定要记得归还

// 传入预计的容量
public ValueStringBuilder(int initialCapacity)  
{  // 从对象池中获取缓冲区_arrayToReturnToPool = ArrayPool<char>.Shared.Rent(initialCapacity);  ......
}

那么我们就来比较一下使用+=StringBuilderValueStringBuilder这几种方式的性能吧。

// 一个简单的类
public class SomeClass  
{  public int Value1; public int Value2; public float Value3;  public double Value4; public string? Value5; public decimal Value6;  public DateTime Value7; public TimeOnly Value8; public DateOnly Value9;  public int[]? Value10;  
}
// Benchmark类
[MemoryDiagnoser]  
[HtmlExporter]  
[Orderer(SummaryOrderPolicy.FastestToSlowest)]  
public class StringBuilderBenchmark  
{  private static readonly SomeClass Data;  static StringBuilderBenchmark()  {  var baseTime = DateTime.Now;  Data = new SomeClass  {  Value1 = 100, Value2 = 200, Value3 = 333,  Value4 = 400, Value5 = string.Join('-', Enumerable.Range(0, 10000).Select(i => i.ToString())),  Value6 = 655, Value7 = baseTime.AddHours(12),  Value8 = TimeOnly.MinValue, Value9 = DateOnly.MaxValue,  Value10 = Enumerable.Range(0, 5).ToArray()  };  }// 使用我们熟悉的StringBuilder[Benchmark(Baseline = true)]  public string StringBuilder()  {  var data = Data;  var sb = new StringBuilder();  sb.Append("Value1:"); sb.Append(data.Value1);  if (data.Value2 > 10)  {  sb.Append(" ,Value2:"); sb.Append(data.Value2);  }  sb.Append(" ,Value3:"); sb.Append(data.Value3);  sb.Append(" ,Value4:"); sb.Append(data.Value4);  sb.Append(" ,Value5:"); sb.Append(data.Value5);  if (data.Value6 > 20)  {  sb.Append(" ,Value6:"); sb.AppendFormat("{0:F2}", data.Value6);  }  sb.Append(" ,Value7:"); sb.AppendFormat("{0:yyyy-MM-dd HH:mm:ss}", data.Value7);  sb.Append(" ,Value8:"); sb.AppendFormat("{0:HH:mm:ss}", data.Value8);  sb.Append(" ,Value9:"); sb.AppendFormat("{0:yyyy-MM-dd}", data.Value9);  sb.Append(" ,Value10:");  if (data.Value10 is null or {Length: 0}) return sb.ToString();  for (int i = 0; i < data.Value10.Length; i++)  {  sb.Append(data.Value10[i]);  }  return sb.ToString();  }// StringBuilder使用Capacity[Benchmark]  public string StringBuilderCapacity()  {  var data = Data;  var sb = new StringBuilder(20480);  sb.Append("Value1:"); sb.Append(data.Value1);  if (data.Value2 > 10)  {  sb.Append(" ,Value2:"); sb.Append(data.Value2);  }  sb.Append(" ,Value3:"); sb.Append(data.Value3);  sb.Append(" ,Value4:"); sb.Append(data.Value4);  sb.Append(" ,Value5:"); sb.Append(data.Value5);  if (data.Value6 > 20)  {  sb.Append(" ,Value6:"); sb.AppendFormat("{0:F2}", data.Value6);  }  sb.Append(" ,Value7:"); sb.AppendFormat("{0:yyyy-MM-dd HH:mm:ss}", data.Value7);  sb.Append(" ,Value8:"); sb.AppendFormat("{0:HH:mm:ss}", data.Value8);  sb.Append(" ,Value9:"); sb.AppendFormat("{0:yyyy-MM-dd}", data.Value9);  sb.Append(" ,Value10:");  if (data.Value10 is null or {Length: 0}) return sb.ToString();  for (int i = 0; i < data.Value10.Length; i++)  {  sb.Append(data.Value10[i]);  }  return sb.ToString();  }  // 直接使用+=拼接字符串[Benchmark]  public string StringConcat()  {  var str = "";  var data = Data;  str += ("Value1:"); str += (data.Value1);  if (data.Value2 > 10)  {  str += " ,Value2:"; str += data.Value2;  }  str += " ,Value3:"; str += (data.Value3);  str += " ,Value4:"; str += (data.Value4);  str += " ,Value5:"; str += (data.Value5);  if (data.Value6 > 20)  {  str += " ,Value6:"; str += data.Value6.ToString("F2");  }  str += " ,Value7:"; str += data.Value7.ToString("yyyy-MM-dd HH:mm:ss");  str += " ,Value8:"; str += data.Value8.ToString("HH:mm:ss");  str += " ,Value9:"; str += data.Value9.ToString("yyyy-MM-dd");  str += " ,Value10:";  if (data.Value10 is not null && data.Value10.Length > 0)  {  for (int i = 0; i < data.Value10.Length; i++)  {  str += (data.Value10[i]);  }     }  return str;  }  // 使用栈上分配的ValueStringBuilder[Benchmark]  public string ValueStringBuilderOnStack()  {  var data = Data;  Span<char> buffer = stackalloc char[20480];  var sb = new ValueStringBuilder(buffer);  sb.Append("Value1:"); sb.AppendSpanFormattable(data.Value1);  if (data.Value2 > 10)  {  sb.Append(" ,Value2:"); sb.AppendSpanFormattable(data.Value2);  }  sb.Append(" ,Value3:"); sb.AppendSpanFormattable(data.Value3);  sb.Append(" ,Value4:"); sb.AppendSpanFormattable(data.Value4);  sb.Append(" ,Value5:"); sb.Append(data.Value5);  if (data.Value6 > 20)  {  sb.Append(" ,Value6:"); sb.AppendSpanFormattable(data.Value6, "F2");  }  sb.Append(" ,Value7:"); sb.AppendSpanFormattable(data.Value7, "yyyy-MM-dd HH:mm:ss");  sb.Append(" ,Value8:"); sb.AppendSpanFormattable(data.Value8, "HH:mm:ss");  sb.Append(" ,Value9:"); sb.AppendSpanFormattable(data.Value9, "yyyy-MM-dd");  sb.Append(" ,Value10:");  if (data.Value10 is not null && data.Value10.Length > 0)  {  for (int i = 0; i < data.Value10.Length; i++)  {  sb.AppendSpanFormattable(data.Value10[i]);  }     }  return sb.ToString();  }// 使用ArrayPool 堆上分配的StringBuilder[Benchmark]  public string ValueStringBuilderOnHeap()  {  var data = Data;  var sb = new ValueStringBuilder(20480);  sb.Append("Value1:"); sb.AppendSpanFormattable(data.Value1);  if (data.Value2 > 10)  {  sb.Append(" ,Value2:"); sb.AppendSpanFormattable(data.Value2);  }  sb.Append(" ,Value3:"); sb.AppendSpanFormattable(data.Value3);  sb.Append(" ,Value4:"); sb.AppendSpanFormattable(data.Value4);  sb.Append(" ,Value5:"); sb.Append(data.Value5);  if (data.Value6 > 20)  {  sb.Append(" ,Value6:"); sb.AppendSpanFormattable(data.Value6, "F2");  }  sb.Append(" ,Value7:"); sb.AppendSpanFormattable(data.Value7, "yyyy-MM-dd HH:mm:ss");  sb.Append(" ,Value8:"); sb.AppendSpanFormattable(data.Value8, "HH:mm:ss");  sb.Append(" ,Value9:"); sb.AppendSpanFormattable(data.Value9, "yyyy-MM-dd");  sb.Append(" ,Value10:");  if (data.Value10 is not null && data.Value10.Length > 0)  {  for (int i = 0; i < data.Value10.Length; i++)  {  sb.AppendSpanFormattable(data.Value10[i]);  }     }return sb.ToString();  }}

结果如下所示。343eecb2ca08f1f44710b91b5a74f644.png从上图的结果中,我们可以得出如下的结论。

  • 使用StringConcat是最慢的,这种方式是无论如何都不推荐的。

  • 使用StringBuilder要比使用StringConcat快6.5倍,这是推荐的方法。

  • 设置了初始容量的StringBuilder要比直接使用StringBuilder快25%,正如我在你应该为集合类型设置初始大小[1]一样,设置初始大小绝对是相当推荐的做法。

  • 栈上分配的ValueStringBuilderStringBuilder要快50%,比设置了初始容量的StringBuilder还快25%,另外它的GC次数是最低的。

  • 堆上分配的ValueStringBuilderStringBuilder要快55%,他的GC次数稍高与栈上分配。 从上面的结论中,我们可以发现ValueStringBuilder的性能非常好,就算是在栈上分配缓冲区,性能也比StringBuilder快25%。

源码解析

ValueStringBuilder的源码不长,我们挑几个重要的方法给大家分享一下,部分源码如下。

// 使用 ref struct 该对象只能在栈上分配
public ref struct ValueStringBuilder
{// 如果从ArrayPool里分配buffer 那么需要存储一下// 以便在Dispose时归还private char[]? _arrayToReturnToPool;// 暂存外部传入的bufferprivate Span<char> _chars;// 当前字符串长度private int _pos;// 外部传入bufferpublic ValueStringBuilder(Span<char> initialBuffer){// 使用外部传入的buffer就不使用从pool里面读取的了_arrayToReturnToPool = null;_chars = initialBuffer;_pos = 0;}public ValueStringBuilder(int initialCapacity){// 如果外部传入了capacity 那么从ArrayPool里面获取_arrayToReturnToPool = ArrayPool<char>.Shared.Rent(initialCapacity);_chars = _arrayToReturnToPool;_pos = 0;}// 返回字符串的Length 由于Length可读可写// 所以重复使用ValueStringBuilder只需将Length设置为0public int Length{get => _pos;set{Debug.Assert(value >= 0);Debug.Assert(value <= _chars.Length);_pos = value;}}......[MethodImpl(MethodImplOptions.AggressiveInlining)]public void Append(char c){// 添加字符非常高效 直接设置到对应Span位置即可int pos = _pos;if ((uint) pos < (uint) _chars.Length){_chars[pos] = c;_pos = pos + 1;}else{// 如果buffer空间不足,那么会走GrowAndAppend(c);}}[MethodImpl(MethodImplOptions.AggressiveInlining)]public void Append(string? s){if (s == null){return;}// 追加字符串也是一样的高效int pos = _pos;// 如果字符串长度为1 那么可以直接像追加字符一样if (s.Length == 1 && (uint) pos < (uint) _chars .Length){_chars[pos] = s[0];_pos = pos + 1;}else{// 如果是多个字符 那么使用较慢的方法AppendSlow(s);}}private void AppendSlow(string s){// 追加字符串 空间不够先扩容// 然后使用Span复制 相当高效int pos = _pos;if (pos > _chars.Length - s.Length){Grow(s.Length);}s
#if !NETCOREAPP.AsSpan()
#endif.CopyTo(_chars.Slice(pos));_pos += s.Length;}// 对于需要格式化的对象特殊处理[MethodImpl(MethodImplOptions.AggressiveInlining)]public void AppendSpanFormattable<T>(T value, string? format = null, IFormatProvider? provider = null)where T : ISpanFormattable{// ISpanFormattable非常高效if (value.TryFormat(_chars.Slice(_pos), out int charsWritten, format, provider)){_pos += charsWritten;}else{Append(value.ToString(format, provider));}}[MethodImpl(MethodImplOptions.NoInlining)]private void GrowAndAppend(char c){// 单个字符扩容在添加Grow(1);Append(c);}// 扩容方法[MethodImpl(MethodImplOptions.NoInlining)]private void Grow(int additionalCapacityBeyondPos){Debug.Assert(additionalCapacityBeyondPos > 0);Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos,"Grow called incorrectly, no resize is needed.");// 同样也是2倍扩容,默认从对象池中获取bufferchar[] poolArray = ArrayPool<char>.Shared.Rent((int) Math.Max((uint) (_pos + additionalCapacityBeyondPos),(uint) _chars.Length * 2));_chars.Slice(0, _pos).CopyTo(poolArray);char[]? toReturn = _arrayToReturnToPool;_chars = _arrayToReturnToPool = poolArray;if (toReturn != null){// 如果原本就是使用的对象池 那么必须归还ArrayPool<char>.Shared.Return(toReturn);}}// [MethodImpl(MethodImplOptions.AggressiveInlining)]public void Dispose(){char[]? toReturn = _arrayToReturnToPool;this = default; // 为了安全,在释放时置空当前对象if (toReturn != null){// 一定要记得归还对象池ArrayPool<char>.Shared.Return(toReturn);}}
}

从上面的源码我们可以总结出ValueStringBuilder的几个特征:

  • 比起StringBuilder来说,实现方式非常简单。

  • 一切都是为了高性能,比如各种Span的用法,各种内联参数,以及使用对象池等等。

  • 内存占用非常低,它本身就是结构体类型,另外它是ref struct,意味着不会被装箱,不会在堆上分配。

适用场景

ValueStringBuilder是一种高性能的字符串创建方式,针对于不同的场景,可以有不同的使用方式。1.非常高频次的字符串拼接的场景,并且字符串长度较小此时可以使用栈上分配ValueStringBuilder。 大家都知道现在ASP.NET Core性能非常好,在其依赖的内部库UrlBuilder[2]中,就使用栈上分配,因为栈上分配在当前方法结束后内存就会回收,所以不会造成任何GC压力。8406d8374815a3f4b598ecb9d156d31a.png2.非常高频次的字符串拼接场景,但是字符串长度不可控此时使用ArrayPool指定容量ValueStringBuilder。比如在.NET BCL库中有很多场景使用,比如动态方法的ToString[3]实现。从池中分配虽然没有栈上分配那么高效,但是一样的能降低内存占用和GC压力。9c16b9fc17670536f49e8678ed76311c.png3. 非常高频次的字符串拼接场景,但是字符串长度可控,此时可以栈上分配和ArrayPool分配联合使用,比如正则表达式[4]解析类中,如果字符串长度较小那么使用栈空间,较大那么使用ArrayPool。d79671fe682a5abe8f963c1b0fe60b6d.png

需要注意的场景

1.在async\await中无法使用ValueStringBuilder。原因大家也都知道,因为ValueStringBuilderref struct它只能在栈上分配async\await会编译成状态机拆分await前后的方法,所以ValueStringBuilder不好在方法内传递,不过编译器也会警告。558667c34a3163bab84a844e66bbccea.png2.无法将ValueStringBuilder作为返回值返回,因为在当前栈上分配,方法结束后它会被释放,返回它将指向未知的地址。这个编译器也会警告。3a858dacc12a8ef6964711367cabbb26.png3.如果要将ValueStringBuilder传递给其它方法,那么必须使用ref传递,否则发生值拷贝会存在多个实例。这个编译器不会警告,但是你必须非常注意。97a24949bc7a6e890521dbf1a2953ed4.png4. 如果使用栈上分配,那么Buffer大小控制在5KB内比较稳妥,至于为什么需要这样,后面有机会在讲一讲。

总结

今天和大家分享了一下高性能几乎无内存占用的字符串拼接结构体ValueStringBuilder,在大多数的场景还是推荐大家使用。但是要非常注意上面提到的[5]的几个场景,如果不符合条件,那么大家还是可以使用高效的StringBuilder来进行字符串拼接。

本文源码链接

https://github.com/InCerryGit/BlogCode-Use-ValueStringBuilder

参考资料

[1]

你应该为集合类型设置初始大小: https://www.cnblogs.com/InCerry/p/Dotnet-Opt-Perf-You-Should-Set-Capacity-For-Collection.html

[2]

UrlBuilder: https://github.com/dotnet/runtime/blob/57bfe474518ab5b7cfe6bf7424a79ce3af9d6657/src/libraries/System.Private.Uri/src/System/UriBuilder.cs#L284-L362

[3]

ToString: https://github.com/dotnet/runtime/blob/43dd0a74ab524278620d8c6a9d33a9b73b2d2228/src/coreclr/System.Private.CoreLib/src/System/Reflection/RuntimeMethodInfo.CoreCLR.cs#L137

[4]

正则表达式: https://github.com/dotnet/runtime/blob/43dd0a74ab524278620d8c6a9d33a9b73b2d2228/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexParser.cs#L150

[5]

上面提到的: #需要注意的场景

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/281521.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

STOLUCK:经济下行的当下 ,STO或将帮助中小企业度过寒冬

2018年被称为创业阵亡率特别高的一年&#xff0c;相关报道称有近20%的创业团队面临“后续融资跟不上&#xff0c;可能死在春天来临之前”的窘境。经济不景气的当下&#xff0c;上下游资金不足&#xff0c;信贷机构没钱&#xff0c;风投业捉襟见肘。实际今年3月份开始&#xff0…

浅析C# Dictionary实现原理

一、前言二、理论知识1、Hash 算法2、Hash 桶算法3、解决冲突算法三、Dictionary 实现1. Entry 结构体2. 其它关键私有变量3. Dictionary - Add 操作4. Dictionary - Find 操作5. Dictionary - Remove 操作6. Dictionary - Resize 操作(扩容)7. Dictionary - 再谈 Add 操作8. C…

猫晚流量再创记录,阿里云直播方案护航优酷2500万用户体验

2019独角兽企业重金招聘Python工程师标准>>> 对“剁手党而言&#xff0c;天猫双11早已经超越了简单的“买买买”&#xff0c;更是一场边看边玩的狂欢盛宴。今年的天猫双11狂欢夜晚会&#xff08;简称“猫晚”&#xff09;在上海举办&#xff0c;这台兼具年轻潮流与国…

python实现二叉树和它的七种遍历

介绍&#xff1a; 树是数据结构中非常重要的一种&#xff0c;主要的用途是用来提高查找效率&#xff0c;对于要重复查找的情况效果更佳&#xff0c;如二叉排序树、FP-树。另外可以用来提高编码效率&#xff0c;如哈弗曼树。 代码&#xff1a; 用python实现树的构造和几种遍历算…

.NET性能系列文章二:Newtonsoft.Json vs System.Text.Json

微软终于追上了&#xff1f;图片来自 Glenn Carstens-Peters[1]Unsplash[2]欢迎来到.NET 性能系列的另一章。这个系列的特点是对.NET 世界中许多不同的主题进行研究、基准和比较。正如标题所说的那样&#xff0c;重点在于使用最新的.NET7 的性能。你将看到哪种方法是实现特定主…

android gpu平板 推荐,性能强的不像话,最强安卓平板华为平板M6上手

原标题&#xff1a;性能强的不像话&#xff0c;最强安卓平板华为平板M6上手你为什么买平板电脑&#xff1f;当这一问题问出以后&#xff0c;许多朋友的表情都很微妙&#xff0c;随后大概率的回答则相当统一&#xff1a;"我买平板干嘛&#xff1f;"。其实得到这样一个…

企业应用“数据优先”革命的下一个主战场:安全与运营

根据IDC发布的2015年全球CIO日程预测&#xff0c;80%的CIO将提供一个实现创新和改善业务决策的新体系架构。 大数据时代&#xff0c;企业软件市场正在经历一次大迁移&#xff0c;数以十亿计的企业IT支出预算将投向“数据优先”应用&#xff0c;而不是长久以来以业务流程和工作流…

给Web开发人员的以太坊入坑指南

以太坊现在各种学习资料数不胜数&#xff0c;但由于以太坊正处于飞速发展阶段&#xff0c;有些学习资料很快就过时了。所以想找到有价值的资料无异于大海捞针。我费了很大功夫&#xff0c;才建立起对以太坊的整体认识&#xff0c;搞清楚它的工作机制。我相信很多跃跃欲试的开发…

一款简单的缩放拖拽图片控件

本文介绍一个针对 .NET 桌面应用程序的独立图片缩放拖拽显示控件 SQPhoto[1]。SQPhoto 是一个 Windows 桌面应用的组件&#xff0c;支持 .NET6 和 .NET Framework 4.6 。基于 PictureBox 的图片展示工具&#xff0c;增加了拖动和缩放功能&#xff0c;便于在某些场景下的图片展…

HTML怎么让div全透明,设置div为透明 怎样才让div里面的div不透明?

#a{ background:#FFCC33; filter:alpha(opacity:0); width: 300px; heig#a{background:#FFCC33; filter:alpha(opacity50); /*支持 IE 浏览器*/-moz-opacity:0.50; /*支持 FireFox 浏览器*/opacity:0.50; /*支持 Chrome, Opera, Safari 等浏览器*/width: 300px;height:300px;}还…

html overflow 样式,css样式之overflow-x属性样式

overflow-x是overflow子花样&#xff0c;平日也很少用的。overflow-x设置匿伏溢出过宽模式(比如过宽图片)、设置对象底部转折条等重要。overflow-x语法与根本懂得1、overflow-x可设置值overflow-x : visible | auto | hidden| scroll值与解释引见&#xff1a;visible :  不剪切…

C# WPF GridControl用法举例

概述GridControl是Dev中的表格控件&#xff0c;类似于Winfrom中的DataGridView&#xff0c;以及WPF中的DataGrid&#xff0c;但是这个控件功能比原生的功能要强大很多&#xff0c;下面用实例举例说明此控件的用法.代码前台XAML&#xff1a;<UserControl x:Class"Calibu…

js中关于Blob对象的介绍与使用

js中关于Blob对象的介绍与使用 blob对象介绍 一个 Blob对象表示一个不可变的, 原始数据的类似文件对象。Blob表示的数据不一定是一个JavaScript原生格式 blob对象本质上是js中的一个对象&#xff0c;里面可以储存大量的二进制编码格式的数据。 创建blob对象 创建blob对象本质上…

20170102-文件处理

文件处理 正常文件处理 python 文件处理 编码 f open(file"兼职白领学生空姐模特护士练习方式.txt",mode"r",encoding"utf-8")#把值附给变量f &#xff08;路径file"文件是兼职白领学生空姐模特护士练习方式.txt"&#xff0c;文本模式…

创建.NET程序Dump的几种姿势

当一个应用程序运行的有问题时&#xff0c;生成一个 Dump 文件来调试它可能会很有用。在 Windows、Linux 或 Azure 上有许多方法可以生成转储文件。Windows 平台dotnet-dump (Windows)dotnet-dump 全局工具[1]是一种收集和分析.NET 核心应用程序 Dump 的方法。安装 dotnet-dump…

自然语言处理怎么最快入门?

2019独角兽企业重金招聘Python工程师标准>>> 本文整理自知乎上的一个问答&#xff0c;分享给正在学习自然然语言处理的朋友们&#xff01; 一、自然语言处理是什么&#xff1f; 自然语言处理说白了&#xff0c;就是让机器去帮助我们完成一些语言层面的事情&#xff…

dotnet-exec 0.8.0 released

dotnet-exec 0.8.0 releasedIntrodotnet-exec 是一个 C# 程序的小工具&#xff0c;可以用来运行一些简单的 C# 程序而无需创建项目文件&#xff0c;而且可以自定义项目的入口方法&#xff0c;支持但不限于 Main 方法Install/Updatedotnet-exec 是一个 dotnet tool&#xff0c;可…

Siamese Network理解

提起siamese network一般都会引用这两篇文章&#xff1a; 《Learning a similarity metric discriminatively, with application to face verification》和《 Hamming Distance Metric Learning》。 本文主要通过论文《Learning a Similarity Metric Discriminatively, with A…

HashMap是如何工作的

2019独角兽企业重金招聘Python工程师标准>>> 1 HashMap在JAVA中的怎么工作的&#xff1f; 基于Hash的原理 2 什么是哈希&#xff1f; 最简单形式的 hash&#xff0c;是一种在对任何变量/对象的属性应用任何公式/算法后&#xff0c; 为其分配唯一代码的方法。 一个真…

如何做到十五分钟领略PowerBI的DAX精华及框架

有小伙伴问&#xff0c;如何最快速理解整套 DAX 精华体系&#xff0c;例如&#xff1a;15分钟。这虽然是个不可能完成的任务&#xff0c;但在借助脑图PPT的强大能力下&#xff0c;还是可以做到的。如果你从没接触过 DAX&#xff0c;但未来要学习 DAX&#xff0c;以下视频值得看…