C#性能优化总结


一、C#语言方面

1.1、垃圾回收

垃圾回收解放了手工管理对象的工作,提高了程序的健壮性,但副作用就是程序代码可能对于对象创建变得随意。

1.1.1、避免不必要的对象创建

由于垃圾回收的代价较高,所以C#程序开发要遵循的一个基本原则就是避免不必要的对象创建。以下列举一些常见的情形。

  • 避免循环创建对象 ★

如果对象并不会随每次循环而改变状态,那么在循环中反复创建对象将带来性能损耗。高效的做法是将对象提到循环外面创建。

  • 在需要逻辑分支中创建对象

如果对象只在某些逻辑分支中才被用到,那么应只在该逻辑分支中创建对象。

  • 使用常量避免创建对象

程序中不应出现如 new Decimal(0) 之类的代码,这会导致小对象频繁创建及回收,正确的做法是使用Decimal.Zero常量。我们有设计自己的类时,也可以学习这个设计手法,应用到类似的场景中。

  • 使用StringBuilder做字符串连接

1.1.2、不要使用空析构函数 ★

如果类包含析构函数,由创建对象时会在 Finalize 队列中添加对象的引用,以保证当对象无法可达时,仍然可以调用到 Finalize 方法。垃圾回收器在运行期间,会启动一个低优先级的线程处理该队列。相比之下,没有析构函数的对象就没有这些消耗。如果析构函数为空,这个消耗就毫无意 义,只会导致性能降低!因此,不要使用空的析构函数。

在实际情况中,许多曾在析构函数中包含处理代码,但后来因为种种原因被注释掉或者删除掉了,只留下一个空壳,此时应注意把析构函数本身注释掉或删除掉。

1.1.3、实现 IDisposable 接口

垃圾回收事实上只支持托管内在的回收,对于其他的非托管资源,例如 Window GDI 句柄或数据库连接,在析构函数中释放这些资源有很大问题。原因是垃圾回收依赖于内在紧张的情况,虽然数据库连接可能已濒临耗尽,但如果内存还很充足的话, 垃圾回收是不会运行的。

C#的 IDisposable 接口是一种显式释放资源的机制。通过提供 using 语句,还简化了使用方式(编译器自动生成 try ... finally 块,并在 finally 块中调用 Dispose 方法)。对于申请非托管资源对象,应为其实现 IDisposable 接口,以保证资源一旦超出 using 语句范围,即得到及时释放。这对于构造健壮且性能优良的程序非常有意义!

为防止对象的 Dispose 方法不被调用的情况发生,一般还要提供析构函数,两者调用一个处理资源释放的公共方法。同时,Dispose 方法应调用 System.GC.SuppressFinalize(this),告诉垃圾回收器无需再处理 Finalize 方法了。

1.2、String 操作

  • 使用 StringBuilder 做字符串连接

string是不变类,使用 + 操作连接字符串将会导致创建一个新的字符串。如果字符串连接次数不是固定的,例如在一个循环中,则应该使用 StringBuilder 类来做字符串连接工作。因为 StringBuilder 内部有一个 StringBuffer ,连接操作不会每次分配新的字符串空间。只有当连接后的字符串超出 Buffer 大小时,才会申请新的 Buffer 空间。典型代码如下:

C# 全选
StringBuilder sb = new StringBuilder(256 );
for ( int i = 0 ; i < Results.Count; i ++ )
{
   sb.Append (Results[i]);
}

如果连接次数是固定的并且只有几次,此时应该直接用 + 号连接,保持程序简洁易读。实际上,编译器已经做了优化,会依据加号次数调用不同参数个数的 String.Concat 方法。例如:

C# 全选
string str = str1 + str2 + str3 + str4;

会被编译为 String.Concat(str1, str2, str3, str4)。该方法内部会计算总的 String 长度,仅分配一次,并不会如通常想象的那样分配三次。作为一个经验值,当字符串连接操作达到 10 次以上时,则应该使用 StringBuilder。

这里有一个细节应注意:StringBuilder 内部 Buffer 的缺省值为 16 ,这个值实在太小。按 StringBuilder 的使用场景,Buffer 肯定得重新分配。经验值一般用 256 作为 Buffer 的初值。当然,如果能计算出最终生成字符串长度的话,则应该按这个值来设定 Buffer 的初值。使用 new StringBuilder(256) 就将 Buffer 的初始长度设为了256。

  • 避免不必要的调用ToUpper 或ToLower 方法

String是不变类,调用ToUpper或ToLower方法都会导致创建一个新的字符串。如果被频繁调用,将导致频繁创建字符串对象。这违背了前面讲到的“避免频繁创建对象”这一基本原则。

例如,bool.Parse方法本身已经是忽略大小写的,调用时不要调用ToLower方法。

另一个非常普遍的场景是字符串比较。高效的做法是使用 Compare 方法,这个方法可以做大小写忽略的比较,并且不会创建新字符串。

还有一种情况是使用 HashTable 的时候,有时候无法保证传递 key 的大小写是否符合预期,往往会把 key 强制转换到大写或小写方法。实际上 HashTable 有不同的构造形式,完全支持采用忽略大小写的 key: new HashTable(StringComparer.OrdinalIgnoreCase)。

  • 最快的空串比较方法

将String对象的Length属性与0比较是最快的方法:if (str.Length == 0)

其次是与String.Empty常量或空串比较:if (str == String.Empty)或if (str == "")

 注意

C#在编译时会将程序集中声明的所有字符串常量放到保留池中(intern pool),相同常量不会重复分配。

1.3、多线程

1.3.1、线程同步

线 程同步是编写多线程程序需要首先考虑问题。

C#为同步提供了 Monitor、Mutex、AutoResetEvent 和 ManualResetEvent 对象来分别包装 Win32 的临界区、互斥对象和事件对象这几种基础的同步机制。

C#还提供了一个lock语句,方便使用,编译器会自动生成适当的 Monitor.Enter 和 Monitor.Exit 调用。

  • 同步粒度

同步粒度可以是整个方法,也可以是方法中某一段代码。

为方法指定 MethodImplOptions.Synchronized 属性将标记对整个方法同步。例如:

C# 全选
[MethodImpl(MethodImplOptions.Synchronized)]
public static SerialManager GetInstance()
{
    if (instance == null)
    {
        instance = new SerialManager();
    }
    return instance;
}

通常情况下,应减小同步的范围,使系统获得更好的性能。简单将整个方法标记为同步不是一个好主意,除非能确定方法中的每个代码都需要受同步保护。

  • 同步策略

使用 lock 进行同步,同步对象可以选择 Type、this 或为同步目的专门构造的成员变量。

避免锁定Type★

锁定Type对象会影响同一进程中所有AppDomain该类型的所有实例,这不仅可能导致严重的性能问题,还可能导致一些无法预期的行为。这是一个很不 好的习惯。即便对于一个只包含static方法的类型,也应额外构造一个static的成员变量,让此成员变量作为锁定对象。

避免锁定 this

锁定 this 会影响该实例的所有方法。假设对象 obj 有 A 和 B 两个方法,其中 A 方法使用 lock(this) 对方法中的某段代码设置同步保护。现在,因为某种原因,B 方法也开始使用 lock(this) 来设置同步保护了,并且可能为了完全不同的目的。这样,A 方法就被干扰了,其行为可能无法预知。所以,作为一种良好的习惯,建议避免使用 lock(this) 这种方式。

使用为同步目的专门构造的成员变量

这是推荐的做法。方式就是 new 一个 object 对象, 该对象仅仅用于同步目的。

如果有多个方法都需要同步,并且有不同的目的,那么就可以为些分别建立几个同步成员变量。

  • 集合同步

C#为各种集合类型提供了两种方便的同步机制:Synchronized 包装器和 SyncRoot 属性。

C# 全选
// Creates and initializes a new ArrayList
ArrayList myAL = new ArrayList();
myAL.Add( " The " );
myAL.Add( " quick " );
myAL.Add( " brown " );
myAL.Add( " fox " );
//Creates a synchronized wrapper around the ArrayList
ArrayList mySyncdAL = ArrayList.Synchronized(myAL);

调用 Synchronized 方法会返回一个可保证所有操作都是线程安全的相同集合对象。考虑 mySyncdAL[0] = mySyncdAL[0] + "test" 这一语句,读和写一共要用到两个锁。一般讲,效率不高。推荐使用 SyncRoot 属性,可以做比较精细的控制。

  • 使用 ThreadStatic 替代 NameDataSlot ★

存 取 NameDataSlot 的 Thread.GetData 和 Thread.SetData 方法需要线程同步,涉及两个锁:一个是 LocalDataStore.SetData 方法需要在 AppDomain 一级加锁,另一个是 ThreadNative.GetDomainLocalStore 方法需要在 Process 一级加锁。如果一些底层的基础服务使用了 NameDataSlot,将导致系统出现严重的伸缩性问题。

规避这个问题的方法是使用 ThreadStatic 变量。示例如下:

C# 全选
public sealed class InvokeContext
{
    [ThreadStatic]
    private static InvokeContext current;
    private Hashtable maps = new Hashtable();
}

1.3.3、多线程编程技巧

使用 Double Check 技术创建对象

C# 全选
internal IDictionary KeyTable
{
    get
    {
        if (this._keyTable == null)
        {
            lock (base._lock)
            {
                if (this._keyTable == null)
                {
                    this._keyTable = new Hashtable();
                }
            }
        }
        return this._keyTable;
    }
}

创建单例对象是很常见的一种编程情况。一般在 lock 语句后就会直接创建对象了,但这不够安全。因为在 lock 锁定对象之前,可能已经有多个线程进入到了第一个 if 语句中。如果不加第二个 if 语句,则单例对象会被重复创建,新的实例替代掉旧的实例。如果单例对象中已有数据不允许被破坏或者别的什么原因,则应考虑使用 Double Check 技术。

1.4、类型系统

1.4.1、避免无意义的变量初始化动作

CLR保证所有对象在访问前已初始化,其做法是将分配的内存清零。因此,不需要将变量重新初始化为0、false或null。

需要注意的是:方法中的局部变量不是从堆而是从栈上分配,所以C#不会做清零工作。如果使用了未赋值的局部变量,编译期间即会报警。不要因为有这个印象而对所有类的成员变量也做赋值动作,两者的机理完全不同!

1.4.2、ValueType 和 ReferenceType

  • 以引用方式传递值类型参数

值类型从调用栈分配,引用类型从托管堆分配。当值类型用作方法参数时,默认会进行参数值复制,这抵消了值类型分配效率上的优势。作为一项基本技巧,以引用方式传递值类型参数可以提高性能。

  • 为 ValueType 提供 Equals 方法

.NET默认实现的 ValueType.Equals 方法使用了反射技术,依靠反射来获得所有成员变量值做比较,这个效率极低。

如果我们编写的值对象其 Equals 方法要被用到(例如将值对象放到 HashTable 中),那么就应该重载 Equals 方法。

C# 全选
public struct Rectangle
{
  public double Length;
  public double Breadth;
  public override bool Equals ( object ob)
  {
   if(ob is Rectangle)
       return Equels ((Rectangle)ob))
   else
      return false ;
  }
  private bool Equals (Rectangle rect)
  {
   return this .Length == rect.Length && this .Breadth == rect.Breach;
  }
}
  • 避免装箱和拆箱

C#可以在值类型和引用类型之间自动转换,方法是装箱和拆箱。装箱需要从堆上分配对象并拷贝值,有一定性能消耗。如果这一过程发生在循环中或是作为底层方法被频繁调用,则应该警惕累计的效应。

一种经常的情形出现在使用集合类型时。例如:

C# 全选
ArrayList al = new ArrayList();
for ( int i = 0 ; i < 1000 ; i ++ )
{
   al.Add(i); // Implicitly boxed because Add() takes an object
}
int f = ( int )al[ 0 ]; // The element is unboxed

1.5、异常处理

异常也是现代语言的典型特征。与传统检查错误码的方式相比,异常是强制性的(不依赖于是否忘记了编写检查错误码的代码)、强类型的、并带有丰富的异常信息(例如调用栈)。

  • 不要吃掉异常★

关于异常处理的最重要原则就是:不要吃掉异常。这个问题与性能无关,但对于编写健壮和易于排错的程序非常重要。这个原则换一种说法,就是不要捕获那些你不能处理的异常。

吃掉异常是极不好的习惯,因为你消除了解决问题的线索。一旦出现错误,定位问题将非常困难。除了这种完全吃掉异常的方式外,只将异常信息写入日志文件但并不做更多处理的做法也同样不妥。

  • 不要吃掉异常信息★

有些代码虽然抛出了异常,但却把异常信息吃掉了。

为异常披露详尽的信息是程序员的职责所在。如果不能在保留原始异常信息含义的前提下附加更丰富和更人性化的内容,那么让原始的异常信息直接展示也要强得多。千万不要吃掉异常。

  • 避免不必要的抛出异常

抛出异常和捕获异常属于消耗比较大的操作,在可能的情况下,应通过完善程序逻辑避免抛出不必要不必要的异常。与此相关的一个倾向是利用异常来控制处理逻辑。尽管对于极少数的情况,这可能获得更为优雅的解决方案,但通常而言应该避免。

  • 避免不必要的重新抛出异常

如果是为了包装异常的目的(即加入更多信息后包装成新异常),那么是合理的。但是有不少代码,捕获异常没有做任何处理就再次抛出,这将无谓地增加一次捕获异常和抛出异常的消耗,对性能有伤害。

1.6、反射

反射是一项很基础的技术,它将编译期间的静态绑定转换为延迟到运行期间的动态绑定。在很多场景下(特别是类框架的设计),可以获得灵活易于扩展的架构。但带来的问题是与静态绑定相比,动态绑定会对性能造成较大的伤害。

1.6.1、反射分类

type comparison :类型判断,主要包括 is 和 typeof 两个操作符及对象实例上的 GetType 调用。这是最轻型的消耗,可以无需考虑优化问题。注意 typeof 运算符比对象实例上的 GetType 方法要快,只要可能则优先使用 typeof 运算符。

member enumeration :成员枚举,用于访问反射相关的元数据信息,例如Assembly.GetModule、Module.GetType、Type对象上的 IsInterface、IsPublic、GetMethod、GetMethods、GetProperty、GetProperties、 GetConstructor调用等。尽管元数据都会被CLR缓存,但部分方法的调用消耗仍非常大,不过这类方法调用频度不会很高,所以总体看性能损失程 度中等。

member invocation:成员调用,包括动态创建对象及动态调用对象方法,主要有Activator.CreateInstance、Type.InvokeMember等。

1.6.2、动态创建对象

C#主要支持 5 种动态创建对象的方式:

1. Type.InvokeMember

2. ContructorInfo.Invoke

3. Activator.CreateInstance(Type)

4. Activator.CreateInstance(assemblyName, typeName)

5. Assembly.CreateInstance(typeName)

最快的是方式 3 ,与 Direct Create 的差异在一个数量级之内,约慢 7 倍的水平。其他方式,至少在 40 倍以上,最慢的是方式 4 ,要慢三个数量级。

1.6.3、动态方法调用

方法调用分为编译期的早期绑定和运行期的动态绑定两种,称为Early-Bound Invocation和Late-Bound Invocation。

Early-Bound Invocation可细分为Direct-call、Interface-call和Delegate-call。

Late-Bound Invocation主要有Type.InvokeMember和MethodBase.Invoke,还可以通过使用LCG(Lightweight Code Generation)技术生成IL代码来实现动态调用。

从测试结果看,相比Direct Call,Type.InvokeMember要接近慢三个数量级;MethodBase.Invoke虽然比Type.InvokeMember要快三 倍,但比Direct Call仍慢270倍左右。可见动态方法调用的性能是非常低下的。我们的建议是:除非要满足特定的需求,否则不要使用!

1.6.4 、推荐的使用原则

模式

1. 如果可能,则避免使用反射和动态绑定

2. 使用接口调用方式将动态绑定改造为早期绑定

3. 使用Activator.CreateInstance(Type)方式动态创建对象

4. 使用typeof操作符代替GetType调用

反模式

1. 在已获得Type的情况下,却使用Assembly.CreateInstance(type.FullName)

1.7、基本代码技巧

这里描述一些应用场景下,可以提高性能的基本代码技巧。对处于关键路径的代码,进行这类的优化还是很有意义的。普通代码可以不做要求,但养成一种好的习惯也是有意义的。

1.7.1、循环写法

可以把循环的判断条件用局部变量记录下来。局部变量往往被编译器优化为直接使用寄存器,相对于普通从堆或栈中分配的变量速度快。如果访问的是复杂计算属性 的话,提升效果将更明显。for (int i = 0, j = collection.GetIndexOf(item); i < j; i++)

需要说明的是:这种写法对于CLR集合类的Count属性没有意义,原因是编译器已经按这种方式做了特别的优化。

1.7.2、拼装字符串

拼装好之后再删除是很低效的写法。有些方法其循环长度在大部分情况下为1,这种写法的低效就更为明显了:

C# 全选
public static string ToString(MetadataKey entityKey)
{
  string str = "" ;
  object [] vals = entityKey.values;
  for ( int i = 0 ; i < vals.Length; i ++ )
  {
    str += " , " + vals[i].ToString();
  }
  return str == "" ? "" : str.Remove( 0 , 1 );
}

推荐下面的写法:

C# 全选
if(str.Length == 0 )
str = vals[i].ToString();
else
str += " , " + vals[i].ToString();

其实这种写法非常自然,而且效率很高,完全不需要用个Remove方法绕来绕去。

1.7.3、避免两次检索集合元素

获取集合元素时,有时需要检查元素是否存在。通常的做法是先调用ContainsKey(或Contains)方法,然后再获取集合元素。这种写法非常符合逻辑。

但如果考虑效率,可以先直接获取对象,然后判断对象是否为null来确定元素是否存在。对于Hashtable,这可以节省一次GetHashCode调用和n次Equals比较。

如下面的示例:

C# 全选
public IData GetItemByID(Guid id)
{
  IData data1 = null ;
  if( this .idTable.ContainsKey(id.ToString())
  {
     data1 = this .idTable[id.ToString()] as IData;
  }
  return data1;
}

其实完全可用一行代码完成:return this.idTable[id] as IData;

1.7.4、避免两次类型转换

考虑如下示例,其中包含了两处类型转换:

C# 全选
if (obj is SomeType)
{
  SomeType st = (SomeType)obj;
  st.SomeTypeMethod();
}

效率更高的做法如下:

C# 全选
SomeType st = obj as SomeType;
if (st != null ){
  st.SomeTypeMethod();
}

1.8、Hashtable

Hashtable是一种使用非常频繁的基础集合类型。需要理解影响Hashtable的效率有两个因素:一是散列码(GetHashCode方法),二 是等值比较(Equals方法)。

Hashtable首先使用键的散列码将对象分布到不同的存储桶中,随后在该特定的存储桶中使用键的Equals方法进 行查找。

良好的散列码是第一位的因素,最理想的情况是每个不同的键都有不同的散列码。Equals方法也很重要,因为散列只需要做一次,而存储桶中查找键可能需要做多次。

从实际经验看,使用Hashtable时,Equals方法的消耗一般会占到一半以上。

System.Object类提供了默认的GetHashCode实现,使用对象在内存中的地址作为散列码。

我们遇到过一个用Hashtable来缓存对 象的例子,每次根据传递的OQL表达式构造出一个ExpressionList对象,再调用QueryCompiler的方法编译得到 CompiledQuery对象。以ExpressionList对象和CompiledQuery对象作为键值对存储到Hashtable中。

ExpressionList对象没有重载GetHashCode实现,其超类ArrayList也没有,这样最后用的就是System.Object类 的GetHashCode实现。由于ExpressionList对象会每次构造,因此它的HashCode每次都不同,所以这个 CompiledQueryCache根本就没有起到预想的作用。

这个小小的疏漏带来了重大的性能问题,由于解析OQL表达式频繁发生,导致 CompiledQueryCache不断增长,造成服务器内存泄漏!解决这个问题的最简单方法就是提供一个常量实现,例如让散列码为常量0。

虽然这会导 致所有对象汇聚到同一个存储桶中,效率不高,但至少可以解决掉内存泄漏问题。当然,最终还是会实现一个高效的GetHashCode方法的。

1.9、大批量数据操作

当需要对数据库进行大批量数据操作的时候,推荐使用分批操作的功能,比如一百万条数据将其分为每一万条数据进行数据库操作,而不是每条数据循环去进行操作。

查询 - 分批查询对数据库的压力较小,如果那一张表在这个时候可能其他地方也更新或新增 可能需要考虑增加with(NOLOCK) ,当然如果是EF 就套上读未提交的事务(会变卡) 也可以让查询不加锁。

删除 - 首推根据主健进行删除,因为数据库根据主键的索引查找和删除数据非常快,当然分批更好。

 

转载来源:https://www.cnblogs.com/namelessblog/p/13706651.html 

版权声明:本文为YES开发框架网发布内容,转载请附上原文出处连接
管理员
上一篇:Python 让PIP源使用国内镜像,提升下载速度和安装成功率。
下一篇:python3项目打包成exe可执行程序
评论列表

发表评论

评论内容
昵称:
验证码:
验证码
关联文章

C#性能优化总结
.NET 高效依赖注入:使用 Lazy<T> 和工厂模式优化性能与内存占用
Linq和Lambda 性能对比
ASP.NET Core 服务注入对比:IServiceProvider.GetService vs Lazy<T> 注入性能分析
DBHelper SqlSugar EntityFramework 增删改查 性能对比测试
AI聊天机器人总结,图灵、思知、小爱、小微
nginx初始配置优化
也谈string.Join和StringBuilder的性能比较
记一次数据库查询优化记录
网站SEO优化-网站地图生成,SiteMap
C#的进化——C#发展史、C#1.0-10.0语法系统性梳理、C#与JAVA的对比
显卡测试:测试GPU渲染性能 网站地址
sqlsugar freesql hisql 三个ORM框架性能测试对比
SEO优化:HTML5扩展之微数据与丰富网页摘要
Form表单标题换行优化:el-form-item Label换行界面显示 label-multiline 样式
SAP S/4HANA MM模块培训 18 - 优化采购:采购申请供应源分配与转单入口
C#代码编码规范手册 软件开发规范 开发指南
C#实现INotifyPropertyChanged的优雅封装方案(含简洁写法与自动生成)
论发展的十大关系。总结过去,正视未来!!!
C#未来的一点感悟

热门标签
.NET Core .NET Reactor ag-grid AI发布 api安全 ASP.NET Core C#DLL加密 C#播放声音 C#代码混淆 C#代码加密 ChromeDriver Codex DateTime DBeaver devexpress devTool DLL混淆 edge.js EF EFCore Electron element-ui el-form el-table excel FastReport FileStream FolderBrowerDialog FolderSelectDialog form提交 git gridcontrol gridview input javascript json字符串 JS转换对象JSON jwt JWT授权 linq log Math MCP mitmproxy MVC MySQL Navicat netstat nginx node_modules NSwag Nuget Nuget镜像 number PowerShell pyinstaller python pythoncom python爬虫 python抓包 pywin32 redis Requests-html RestSharp Selenium sql SQL Server Swagger to-cms Visual Studio VSCode vue VueRouter vue路由 VUE页面通讯 Webpack Windows Windows服务 winform wmi xlrd yaml YESCMS YESWEB开发框架 白象 表单提交 播放声音 打开URL 代码混淆 弹窗提醒 端口占用 对象转换 分布式 公共字典 机器码 进程排查 静态资源 开发指南 路由参数 密钥 配置教程 配置文件 权限 人工智能 任务 任务调度 日期间隔 日志 日志记录 省市区 授权验证 数据库 四舍五入 文案 文件读取 文件夹选择 文件目录选择 问题排查 行政区域数据 页面通讯 中间件 CSharp 事务锁 工单系统 并发控制 重复提交 CMS Markdig Markdown markdown-it marked 技术选型 VS Code 开发工具 源代码管理 版本控制 Docker PostgreSQL 时区 部署排查 CMS架构 EF Core 主题系统 二次开发 插件系统 容器 运维命令 镜像清理 Linux NAS 远程挂载 飞牛 fnOS S/4HANA SAP GUI SAP HANA SAP R/3 SAP入门 SAP版本 ERP SAP SAP MM 库存管理 物料管理 采购管理 入门教程 SAP S/4HANA SPRO 企业结构 采购组织 MM01 物料主数据 物料类型 BP分组 业务伙伴 供应商主数据 ME41 RFQ 库存物料 采购流程 ME51 消耗性物料 科目分配 采购申请 AC03 ML81N 外部服务 服务主数据 Business Partner SAP培训 ME51N MM模块 Lean Services MM-SRV 外部服务采购 PIR 供应来源 采购主数据 采购信息记录 ME31K 框架协议 计划协议 采购合同 ME01 供应来源确定 货源清单 MEQ1 供应源确定 配额安排 配额评分 MD04 MD21 MRP 计划文件 需求计划 批量程序 MD01N MD02 MRP Live MD05 MM 物料计划 优化采购 供应源 采购订单 ME2A 供应商确认 采购监控 Flexible Workflow 凭证释放 采购审批 释放策略 实地盘点 物料凭证 货物移动 MIGO 收货 移动类型 已撤回 供应商退货 货物发出 STO 库存转储 转移过账 生产订单 预留 GR/IR MIRO 供应商发票 物流发票校验 OMR2 税码 FI PP SD 实操教程 MRBR OMR6 发票差异 交货成本 后续借记 MI01 实物盘点 盘点差异 公司代码 工厂 组织结构 OMS2 主数据定制 自动科目确定 BP角色 CVI 伙伴确定 编号范围 凭证类型 字段选择 FBN1 OMBT OMC2 会计凭证 OMJJ BOM 委外加工 项目类别L MRKO 供应商寄售 特殊库存K MRKON PIPE Pipeline 特殊库存P ERS MRIS 发票计划 周期性结算 里程碑付款 变更追踪 版本管理 采购凭证 SFTP WebDAV 网盘 飞牛fnOS AMPL HERS MPN 中文教程 库存确定 可用性检查 缺件检查 Output Management 消息确定 输出确定 分割评估 库存计价 评估类别 评估类型 PB00 RM0000 条件技术 采购定价 MM-FI集成 OBYC 库存估价 文本类型 文本采用 EFB EVO MSV SU3 用户参数 发票校验 合同参照 履约保留款 特别总账 预付款 Fiori Launchpad SAP Fiori 应用导航 用户体验 LSMW LTMC Migration Cockpit 数据迁移 BRFplus OPD Output Control My Inbox 审批流程 灵活工作流 SAP PP 外部加工 SAP QM 检验批 质量信息记录 采购收货 SAP PM 维护BOM 维护订单 SAP SD SAP Service 端到端流程 MM模块培训 FI-MM集成 供应商管理 审批配置 FICO入门 SAP FICO 财务配置 供应商税务 预扣税 House Bank 银行对账 客户清账 应收账款 FI控制 验证与替代 印度 GST 税务配置 F110 FBZP EWM入门 SAP EWM 仓库管理 OX14 成本核算 物料评估 后勤配置 物料组 价值更新 数量更新 PP-PI 流程制造 生产计划 容差配置 SAP事务码 SAP基础 TCODE Basis 事务代码 MMNR 编号区间 采购实操 组织架构 OMSF SAP实操 FI配置
联系我们
联系电话:15090125178(微信同号)
电子邮箱:garson_zhang@163.com
站长微信二维码
微信二维码