如何计算一个实例占用多少内存?

news2024/11/20 12:34:32

我们都知道CPU和内存是程序最为重要的两类指标,那么有多少人真正想过这个问题:一个类型(值类型或者引用类型)的实例在内存中究竟占多少字节?我们很多人都回答不上来。其实C#提供了一些用于计算大小的操作符和API,但是它们都不能完全解决我刚才提出的问题。本文提供了一种计算值类型和引用类型实例所占内存字节数量的方法。

一、sizeof操作符

sizeof操作用来确定某个类型对应实例所占用的字节数,但是它只能应用在Unmanaged类型上。所谓的Unmanaged类型仅限于:

  • 原生类型(Primitive Type:Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, UIntPtr, Char, Double, 和Single)
  • Decimal类型
  • 枚举类型
  • 指针类型
  • 只包含Unmanaged类型数据成员的结构体

顾名思义,一个Unmanaged类型是一个值类型,对应的实例不能包含任何一个针对托管对象的引用。如果我们定义如下这样一个泛型方法来调用sizeof操作符,泛型参数T必须添加unmananged约束,而且方法上还得添加unsafe标记。

public static unsafe int SizeOf<T>() where T : unmanaged => sizeof(T);

只有原生类型和枚举类型可以直接使用sizeof操作符,如果将它应用在其他类型(指针和自定义结构体),必须添加/unsafe编译标记,还需要放在unsafe上下文中。

Debug.Assert(sizeof(byte) == 1);
Debug.Assert(sizeof(sbyte) == 1);
Debug.Assert(sizeof(short) == 2);
Debug.Assert(sizeof(ushort) == 2);
Debug.Assert(sizeof(int) == 4);
Debug.Assert(sizeof(uint) == 4);
Debug.Assert(sizeof(long) == 8);
Debug.Assert(sizeof(ulong) == 8);
Debug.Assert(sizeof(char) == 2);
Debug.Assert(sizeof(float) == 4);
Debug.Assert(sizeof(double) == 8);
Debug.Assert(sizeof(bool) == 1);
Debug.Assert(sizeof(decimal) == 16);
Debug.Assert(sizeof(DateTimeKind) == 4);

unsafe

{

    Debug.Assert(sizeof(int*) == 8);
    Debug.Assert(sizeof(DateTime) == 8);
    Debug.Assert(sizeof(DateTimeOffset) == 16);
    Debug.Assert(sizeof(Guid) == 16);
    Debug.Assert(sizeof(Point) == 8);

}

由于如下这个结构体Foobar并不是一个Unmanaged类型,所以程序会出现编译错误。

unsafe
{
    Debug.Assert(sizeof(Foobar) == 16);
}
public struct Foobar
{
    public string Foo;
    public int Bar;
}

二、Marshal.SizeOf方法

静态类型Marshal定义了一系列API用来帮助我们完成非托管内存的分配与拷贝、托管类型和非托管类型之间的转换,以及其他一系列非托管内存的操作(Marshal在计算科学中表示为了数据存储或者传输而将内存对象转换成相应的格式的操作)。静态其中就包括如下4个SizeOf方法重载来确定指定类型或者对象的字节数。

public static class Marshal
{
    public static int SizeOf(object structure);
    public static int SizeOf<T>(T structure);
    public static int SizeOf(Type t);
    public static int SizeOf<T>()
}

Marshal.SizeOf方法虽然对指定的类型没有针对Unmanaged类型的限制,但是依然要求指定一个值类型。如果传入的是一个对象,该对象也必须是对一个值类型的装箱。

object  value = default(Foobar);
Debug.Assert(Marshal.SizeOf<Foobar>() == 16);
Debug.Assert(Marshal.SizeOf(value) == 16);
Debug.Assert(Marshal.SizeOf(typeof(Foobar)) == 16);
Debug.Assert(Marshal.SizeOf(typeof(Foobar)) == 16);

public struct Foobar
{
    public object Foo;
    public object Bar;
}

由于如下这个Foobar被定义成了,所以针对两个SizeOf方法的调用都会抛出ArgumentException异常,并提示:Type 'Foobar' cannot be marshaled as an unmanaged structure; no meaningful size or offset can be computed.

Marshal.SizeOf<Foobar>();
Marshal.SizeOf(new Foobar());

public class Foobar
{
    public object Foo;
    public object Bar;
}

Marshal.SizeOf方法不支持泛型,还对结构体的布局有要求,它支持支SequentialExplicit布局模式。由于如下所示的Foobar结构体采用Auto布局模式(由于非托管环境具有更加严格的内存布局要求,所以不支持Auto这种根据字段成员对内存布局进行“动态规划”的方式),所以针对SizeOf方法的调用还是会抛出和上面一样的ArgumentException异常。

Marshal.SizeOf<Foobar>();

[StructLayout(LayoutKind.Auto)]
public struct Foobar
{
    public int Foo;
    public int Bar;
}

三、Unsafe.SizeOf方法>

静态Unsafe提供了针对非托管内存更加底层的操作,类似的SizeIOf方法同样定义在该类型中。该方法对指定的类型没有任何限制,但是如果你指定的是引用类型,它会返回“指针字节数”(IntPtr.Size)。

public static class Unsafe
{
    public static int SizeOf<T>();
}

Debug.Assert( Unsafe.SizeOf<FoobarStructure>() == 16);
Debug.Assert( Unsafe.SizeOf<FoobarClass>() == 8);

public struct FoobarStructure
{
    public long Foo;
    public long Bar;
}

public class FoobarClass
{
    public long Foo;
    public long Bar;
}

四、可以根据字段成员的类型来计算吗?

我们知道不论是值类型还是引用类型,对应的实例都映射为一段连续的片段(或者直接存储在寄存器)。类型的目的就在于规定了对象的内存布局,具有相同类型的实例具有相同的布局,字节数量自然相同(对于引用类型的字段,它在这段字节序列中只存储引用的地址)。既然字节长度由类型来决定,如果我们能够确定每个字段成员的类型,那么我们不就能够将该类型对应的字节数计算出来吗?实际上是不行的。

Debug.Assert(Unsafe.SizeOf<ValueTuple<byte, byte>>() == 2);
Debug.Assert(Unsafe.SizeOf<ValueTuple<byte, short>>() == 4);
Debug.Assert(Unsafe.SizeOf<ValueTuple<byte, int>>() == 8);
Debug.Assert(Unsafe.SizeOf<ValueTuple<byte, long>>() == 16);

一上面的程序为例,我们知道byte、short、int和long的字节数分别是1、2、4和8,所以一个针对byte的二元组的字节数为2,但是对于一个针对类型组合分别为byte + short,byte + int,byte + long的二元组来说,对应的字节并不是3、5和9,而是3、8和16。因为这涉及内存对齐(memory alignment)的问题。

五、值类型和引用类型的布局

对于完全相同的数据成员,引用类型和子类型的实例所占的字节数也是不同的。如下图所示,值类型实例的字节序列全部用来存储它的字段成员。对于引用类型的实例来说,在字段字节序列前面还存储了类型对应方法表(Method Table)的地址。方法表几乎提供了描述类型的所有元数据,我们正是利用这个引用来确定实例属于何种类型。在最前面,还具有额外的字节,我们将其称为Object Header,它不仅仅用来存储对象的锁定状态,哈希值也可以缓存在这里。当我们创建了一个引用类型变量时,这个变量并不是指向实例所占内存的首字节,而是存放方法表地址的地方

 六、Ldflda指令

上面我们介绍sizeof操作符和静态类型Marshal/Unsafe提供的SizeOf方法均不能真正解决实例占用字节长度的计算。就我目前的了解,这个问题在单纯的C#领域都无法解决,但IL层面提供的Ldflda指令可以帮助我们解决这个问题。顾名思义,Ldflda表示Load Field Address,它可以帮助我们得到实例某个字段的地址。由于这个IL指令在C#中没有对应的API,所以我们只有采用如下的形式采用IL Emit的来使用它。

public class SizeCalculator
{
    private static Func<object?, long[]> GenerateFieldAddressAccessor(FieldInfo[] fields)
    {
        var method = new DynamicMethod(
            name: "GetFieldAddresses",
            returnType: typeof(long[]),
            parameterTypes: new[] { typeof(object) },
            m: typeof(SizeCalculator).Module,
            skipVisibility: true);
        var ilGen = method.GetILGenerator();

        // var addresses = new long[fields.Length + 1];
        ilGen.DeclareLocal(typeof(long[]));
        ilGen.Emit(OpCodes.Ldc_I4, fields.Length + 1);
        ilGen.Emit(OpCodes.Newarr, typeof(long));
        ilGen.Emit(OpCodes.Stloc_0);

        // addresses[0] = address of instace;
        ilGen.Emit(OpCodes.Ldloc_0);
        ilGen.Emit(OpCodes.Ldc_I4, 0);
        ilGen.Emit(OpCodes.Ldarg_0);
        ilGen.Emit(OpCodes.Conv_I8);
        ilGen.Emit(OpCodes.Stelem_I8);

        // addresses[index] = address of field[index + 1];
        for (int index = 0; index < fields.Length; index++)
        {
            ilGen.Emit(OpCodes.Ldloc_0);
            ilGen.Emit(OpCodes.Ldc_I4, index + 1);
            ilGen.Emit(OpCodes.Ldarg_0);
            ilGen.Emit(OpCodes.Ldflda, fields[index]);
            ilGen.Emit(OpCodes.Conv_I8);
            ilGen.Emit(OpCodes.Stelem_I8);
        }

        ilGen.Emit(OpCodes.Ldloc_0);
        ilGen.Emit(OpCodes.Ret);

        return (Func<object?, long[]>)method.CreateDelegate(typeof(Func<object, long[]>));
    }
    ...
}

如上面的代码片段所示,我们在SizeCalculator类型中定了一个GenerateFieldAddressAccessor方法,它会根据指定类型的字段列表生成一个Func<object?, long[]> 类型的委托,该委托帮助我们返回指定对象及其所有字段的内存地址。有了对象自身的地址和每个字段的地址,我们自然就可以得到每个字段的偏移量,进而很容易地计算出整个实例所占内存的字节数。

七、计算值类型的字节数

由于值类型和引用类型在内存中采用不同的布局,我们也需要采用不同的计算方式。由于结构体在内存中字节就是所有字段的内容,所有我们采用一种讨巧的计算方法。假设我们需要结算类型为T的结构体的字节数,那么我们创建一个ValueTuple<T,T>元组,它的第二个字段Item2的偏移量就是结构体T的字节数。具体的计算方式体现在如下这个CalculateValueTypeInstance方法中。

public class SizeCalculator
{
    private static readonly MethodInfo _getDefaultMethod = typeof(SizeCalculator).GetMethod(nameof(GetDefault), BindingFlags.Static | BindingFlags.NonPublic)!;

    public int CalculateValueTypeInstance(Type type)
    {
        var instance = GetDefaultAsObject(type);
        var fields = type.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
            .Where(it => !it.IsStatic)
            .ToArray();

        if (fields.Length == 0) return 0;
        var tupleType = typeof(ValueTuple<,>).MakeGenericType(type, type);
        var tupple = tupleType.GetConstructors()[0].Invoke(new object?[] { instance, instance });
        var addresses = GenerateFieldAddressAccessor(tupleType.GetFields()).Invoke(tupple).OrderBy(it => it).ToArray();
        return (int)(addresses[2] - addresses[0]);
    }

    private static T GetDefault<T>() where T : struct => default!;
    private static object? GetDefaultAsObject(Type type) => _getDefaultMethod.MakeGenericMethod(type).Invoke(null, Array.Empty<object>());
}

如上面的代码片段所示, 假设我们需要计算的结构体类型为T,我们调用GetDefaultAsObject方法以反射的形式得到default(T)对象,进而将ValueTuple<T,T>元组创建出来。在调用GenerateFieldAddressAccessor方法得到用于计算实例及其字段地址的Func<object?, long[]> 委托后,我们将这个元组作为参数调用这个委托。对于得到的三个内存地址,代码元组和第1、2个字段的地址是相同的,我们使用代表Item2的第三个地址减去第一个地址,得到的就是我们希望的结果。

八、计算引用类型字节数

引用类型的字节计算要复杂一些,具体采用这样的思路:我们在得到实例自身和每个字段的地址后,我们对地址进行排序进而得到最后一个字段的偏移量。我们让这个偏移量加上最后一个字段自身的字节数,再补充上必要的“头尾字节”就是我们希望得到的结果,具体计算体现在如下这个CalculateReferneceTypeInstance方法上。

public class SizeCalculator
{
    public int CalculateReferenceTypeInstance(Type type, object instance)
    {
        var fields = GetBaseTypesAndThis(type)
            .SelectMany(type => type.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
            .Where(it => !it.IsStatic).ToArray();

        if (fields.Length == 0) return type.IsValueType ? 0 : 3 * IntPtr.Size;
        var addresses = GenerateFieldAddressAccessor(fields).Invoke(instance);
        var list = new List<FieldInfo>(fields);
        list.Insert(0, null!);
        fields = list.ToArray();
        Array.Sort(addresses, fields);

        var lastFieldOffset = (int)(addresses.Last() - addresses.First());
        var lastField = fields.Last();
        var lastFieldSize = lastField.FieldType.IsValueType ? CalculateValueTypeInstance(lastField.FieldType) : IntPtr.Size;
        var size = lastFieldOffset + lastFieldSize;

        // Round up to IntPtr.Size
        int round = IntPtr.Size - 1;
        return ((size + round) & (~round)) + IntPtr.Size;

        static IEnumerable<Type> GetBaseTypesAndThis(Type? type)
        {
            while (type is not null)
            {
                yield return type;
                type = type.BaseType;
            }
        }
    }
}

如上面的代码片段所示,如果指定的类型没有定义任何字段,CalculateReferneceTypeInstance 返回引用类型实例的最小字节数:3倍地址指针字节数。对于x86架构,一个应用类型对象至少占用12字节,包括ObjectHeader(4 bytes)、方法表指针(bytes)和最少4字节的字段内容(即使没有类型没有定义任何字段,这个4个字节也是必需的)。如果是x64架构,这个最小字节数会变成24,因为方法表指针和最小字段内容变成了8个字节,虽然ObjectHeader的有效内容只占用4个字节,但是前面会添加4个字节的Padding。

对于最后字段所占字节的结算也很简单:如果类型是值类型,那么就调用前面定义的CalculateValueTypeInstance方法进行计算,如果是引用类型,字段存储的内容仅仅是目标对象的内存地址,所以长度就是IntPtr.Size。由于引用类型实例在内存中默认会采用IntPtr.Size对齐,这里也做了相应的处理。最后不要忘了,引用类型实例的引用指向的并不是内存的第一个字节,而是存放方法表指针的字节,所以还得加上ObjecthHeader 字节数(IntPtr.Size)。

九、完整的计算

分别用来计算值类型和引用类型实例字节数的两个方法被用在如下这个SizeOf方法中。由于Ldflda指令的调用需要提供对应的实例,所以该方法除了提供目标类型外,还提供了一个用来获得对应实例的委托。该委托对应的参数是可以缺省的,对于值类型,我们会使用默认值。对于引用类型,我们也会试着使用默认构造函数来创建目标对象。如果没有提供此委托对象,也无法创建目标实例,SizeOf方法会抛出异常。虽然需要提供目标实例,但是计算出的结果只和类型有关,所以我们将计算结果进行了缓存。为了调用方便,我们还提供了另一个泛型的SizeOf<T>方法。

public class SizeCalculator
{
    private static readonly ConcurrentDictionary<Type, int> _sizes = new();
    public static readonly SizeCalculator Instance = new();
    public int SizeOf(Type type, Func<object?>? instanceAccessor = null)
    {
        if (_sizes.TryGetValue(type, out var size)) return size;
        if (type.IsValueType) return _sizes.GetOrAdd(type, CalculateValueTypeInstance);

        object? instance;
        try
        {
            instance = instanceAccessor?.Invoke() ?? Activator.CreateInstance(type);
        }
        catch
        {
            throw new InvalidOperationException("The delegate to get instance must be specified.");
        }

        return _sizes.GetOrAdd(type, type => CalculateReferenceTypeInstance(type, instance));
    }
    public int SizeOf<T>(Func<T>? instanceAccessor = null)
    {
        if (instanceAccessor is null) return SizeOf(typeof(T));
        Func<object?> accessor = () => instanceAccessor();
        return SizeOf(typeof(T), accessor);
    }
}

在如下的代码片段中,我们使用它输出了两个具有相同字段定义的结构体和类型的字节数。

Debug.Assert( SizeCalculator.Instance.SizeOf<FoobarStructure>() == 16);
Debug.Assert( SizeCalculator.Instance.SizeOf<FoobarClass>() == 32);

public struct FoobarStructure
{
    public byte Foo;
    public long Bar;
}

public class FoobarClass
{
    public byte Foo;
    public long Bar;
}

 

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

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

相关文章

【026】C++的内联函数、函数重载、函数的默认参数与占位参数

C的内联函数、函数重载、函数的默认参数与占位参数 引言一、内联函数1.1、声明内联函数1.2、宏函数和内联函数的区别1.3、内联函数的注意事项 二、函数重载2.1、函数重载的概述2.2、函数重载的条件2.3、函数重载的底层实现原理 三、函数的默认参数四、占位参数五、extern "…

STM32单片机TFT显示AD9833 DDS信号发生器语音播报正弦波方波三角波

实践制作DIY- GC0146---TFT显示AD9833 DDS信号发生器 基于STM32单片机设计---TFT显示AD9833 DDS信号发生器 二、功能介绍&#xff1a; 硬件组成&#xff1a;STM32F103C系列最小系统板 1.8寸TFT彩屏AD9833信号模块4*4矩阵键盘DY-SV17F语音播报模块 1.通过4*4键盘来设定频率值和…

ExpertLLaMA:超越Vicuna,通过角色扮演增强指令,显著提升回答质量

本文向大家介绍我们刚刚开源的对话模型及相应的训练数据。 首先是 git Repo 和 paper 链接&#xff0c;欢迎大家给我们⭐star⭐ 论文标题&#xff1a; ExpertPrompting: Instructing Large Language Models to be Distinguished Experts 论文链接&#xff1a; https://arxiv.or…

.Net7矢量化的性能优化

前言 矢量化是性能优化的重要技术&#xff0c;也是寄托硬件层面的优化技术。本篇来看下。文章来源&#xff1a;微软官方博客 概括 一&#xff1a;矢量化支持的问题&#xff1a; 矢量化的System.Runtime.Intrinsics.X86.Sse2.MoveMask 函数和矢量化的Vector128.Create().Extract…

get 、post请求 后台@RequestParam、@RequestBody 接收的方法集合

post、get请求 参数&#xff1a;数组、JSON对象、JSON字符串、地址栏 RequestParam、RequestBody 后端接收 //定义json对象&#xff0c;同时包含数组 var _queryData { jflb:"婚姻家庭纠纷",zlay:"xxxxx",ysCode:["0123","4567"]}…

机柜PDU与普通插座的区别,以及如何选择品牌专业PDU产品详解

PDU&#xff08;Power Distribution Unit&#xff09;&#xff0c;是将来自UPS的输出电流分配到各个IT设备的末端配电设备&#xff0c;是连接供电等基础设施与IT系统、关联机房内所有设备正常运转的关键设备。作为机房用电安全的重要保障&#xff0c;PDU设备的稳定与安全直接关…

FPGA驱动FT601实现USB3.0相机HDMI视频采集 提供工程源码和QT上位机源码

目录 1、前言2、FT601芯片解读和时序分析FT601功能和硬件电路FT601读时序解读FT601写时序解读 3、我这儿的 FT601 USB3.0通信方案4、详细设计方案5、vivado工程详解6、上板调试验证7、福利&#xff1a;工程代码的获取 1、前言 目前USB3.0的实现方案很多&#xff0c;但就简单好…

我们来谈谈udp

"却还有那些洗礼&#xff0c;那几句问候&#xff0c;那份温柔~" 一、 常用的Linux命令 (1) netstat查看网络状态 netstat是一个用来查看网络状态的重要工具,可以携带很多选项。 n 拒绝显示别名&#xff0c;能显示数字的全部转化成数字. l 仅列出有在 Listen…

【unity之IMGUI】所以你还想在百度上搜IMGUI的底层原理是什么吗?

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;uni…

安装python详细步骤(超详细,保姆级,一步一图)

❄️作者介绍&#xff1a;奇妙的大歪❄️ &#x1f380;个人名言&#xff1a;但行前路&#xff0c;不负韶华&#xff01;&#x1f380; &#x1f43d;个人简介&#xff1a;云计算网络运维专业人员&#x1f43d; 前言 作为一个实用学习的主义的学习者&#xff0c;最关心的问题一…

光亚展 | 移远通信Matter解决方案,照亮智能家居产业未来

6月9-12日&#xff0c;第二十八届广州国际照明展览会&#xff08;光亚展&#xff09;在中国进出口商品交易会展馆举行。本次展会以“光未来”为主题&#xff0c;整个照明、灯饰产业链的上下游企业、品牌在此汇聚&#xff0c;共同探讨照明行业未来的发展方向。 作为照明行业智能…

智见|黄铁军:未来的大模型生态中将会只有少数赢家

2023智源大会可谓群星璀璨。中外200余位人工智能顶级专家参会&#xff0c;人工智能领域最关键的人物、机构悉数亮相。 全面、专业、前沿&#xff0c;会场上大咖们观点激荡、多元碰撞&#xff0c;会场下观众们兴奋异常、座无虚席。 会上&#xff0c;北京智源人工智能研究院院长黄…

英语知识点-填空-考试酷

第一章 英语知识填空题 一、语法 1.词法&#xff1a;介词将关系建立在人/物A与人/物B&#xff0c;可能会形成修饰限制关系&#xff0c;有定语成分&#xff1b;介词将关系建立在事A与物B&#xff0c;可能有事A动作发生时环境因素&#xff0c;有状语成分&#xff1b;写作中经常用…

【Vue.js】1711- 深入浅出 Vue3 自定义指令

Vue.js[1] 提供了丰富的指令来简化开发者的工作。除了内置指令外&#xff0c;Vue.js 还支持自定义指令&#xff0c;开发者可以根据自己的需求扩展 Vue.js 的指令库。Vue.js 3.x 相较于 Vue.js 2.x 在自定义指令方面进行了一些改进&#xff0c;本文将介绍 Vue.js 3.x 中自定义指…

HLS 设计数字时钟

绪论 该项目的目标是展示 HLS 在设计数字系统方面的能力。为此&#xff0c;本文展示如何在 HLS 中描述数字时钟。如果有兴趣学习 HLS 编码技术&#xff0c;请参阅&#xff1a; ❝ https://highlevel-synthesis.com/ ❞ ❝ https://www.udemy.com/course/hls-combinational-circ…

2023 年的 5G 和网络安全风险

5G 网络的推出出奇地缓慢。作为一个概念&#xff0c;它于 2016 年推出&#xff0c;但直到 2019 年才在全球范围内推出。 四年后&#xff0c;在大多数国家地区&#xff0c;拥有 5G 设备的人数仍然很少。 不确定采用缓慢背后的原因是负担能力、缺乏必要性还是关于它的严重错误…

【redis】redis集群

这里是redis系列文章之《redis集群》&#xff0c;上一篇文章链接&#xff1a;【redis基础】哨兵_努力努力再努力mlx的博客-CSDN博客 目录 概念 作用 集群算法-分片-槽位slot 槽位与分配的概念及两者的优势 官网介绍分析 槽位 分片 两者的优势 slot槽位映射的三种解决方…

linux eventfd事件通知 比信号量更好用

专栏内容&#xff1a;linux下并发编程个人主页&#xff1a;我的主页座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物&#xff0e; 目录 前言 概述 原理简介 使用场景 接口说明 头文件 参数说明 代码演示 默认参数 …

1740_使用Python+ImageMagick实现图像的批量压缩

全部学习汇总&#xff1a; GreyZhang/python_basic: My learning notes about python. (github.com) 前些年使用Linux的时候为了能够方便地往网络上上传照片&#xff0c;使用shell ImageMagick的组合进行照片的批量压缩一直觉得比较方便。不过&#xff0c;那时候即使这么简单的…

JMeter从入门到精通--开始你的第一个JMeter脚本

JMeter是一款在国外非常流行和受欢迎的开源性能测试工具&#xff0c;像LoadRunner 一样&#xff0c;它也提供了一个利用本地Proxy Server&#xff08;代理服务器&#xff09;来录制生成测试脚本的功能&#xff0c;但是这个功能并不好用。所以在本文中介绍一个更为常用的方法——…