• CPPFormatLibary提升效率的优化原理


         CPPFormatLibary,以下简称FL,介绍:关于CPPFormatLibary

         与stringstream,甚至C库的sprintf系列想比,FL在速度上都有优势,而且是在支持.net格式化风格的基础上。要实现这一点,需要多种优化结合在一起,有代码技巧方面的,也有设计策略上的。下面简要的对这些内容进行讲解:

    1.  Pattern缓存

         在C库函数sprintf中,比如这行代码:

    1          char szBuf[64];
    2          sprintf_s(szBuf, "%d--#--%8.2f--#--%s", 100, -40.2f, " String ");

         格式化字符串"%d--#--%8.2f--#--%s"在每次函数调用的时候,都需要分析一次,依次找出对应的格式化符,在实际开发过程中,多数情况下,格式化字符串并没有任何不同,因此这个分析属于重复分析。因此在设计FL时将这样的格式化字符串称为PatternList,并使用Hash容器对这个PatternList进行存储,在每次格式化之前,首先在容器中查找对应字符串的Pattern是否已经存在,有的话则直接使用已经分析的结果。

        下面的代码是Pattern的定义,PatternList则为对应的数组:

     1         /**
     2         * @brief This is the description of a Format unit
     3         * @example {0} {0:d}
     4         */
     5         template < typename TCharType >
     6         class TFormatPattern
     7         {
     8         public:
     9             typedef TCharType                                    CharType;
    10             typedef unsigned char                                ByteType;
    11             typedef std::size_t                                  SizeType;
    12 
    13             enum EFormatFlag
    14             {
    15                 FF_Raw,
    16                 FF_None,
    17                 FF_Decimal,
    18                 FF_Exponent,
    19                 FF_FixedPoint,
    20                 FF_General,
    21                 FF_CSV,
    22                 FF_Percentage,
    23                 FF_Hex
    24             };
    25 
    26             enum EAlignFlag
    27             {
    28                 AF_Right,
    29                 AF_Left
    30             };
    31 
    32             TFormatPattern() :
    33                 Start((SizeType)-1),
    34                 Len(0),
    35                 Flag(FF_Raw),
    36                 Align(AF_Right),
    37                 Index((ByteType)-1),
    38                 Precision((ByteType)-1),
    39                 Width((ByteType)-1)
    40 
    41             {
    42             }
    43 
    44             SizeType  GetLegnth() const
    45             {
    46                 return Len;
    47             }
    48 
    49             bool    IsValid() const
    50             {
    51                 return Start != -1 && Len != -1 && Index >= 0;
    52             }
    53 
    54             bool    HasWidth() const
    55             {
    56                 return Width != (ByteType)-1;
    57             }
    58 
    59             bool    HasPrecision() const
    60             {
    61                 return Precision != (ByteType)-1;
    62             }
    63 
    64         public:
    65             SizeType      Start;
    66             SizeType      Len;
    67             EFormatFlag   Flag;
    68             EAlignFlag    Align;
    69 
    70             ByteType      Index;
    71             ByteType      Precision;
    72             ByteType      Width;
    73         };

         这个Pattern就代表了分析格式化字符串的每一个单元。

    1         StandardLibrary::FormatTo(str, "{0}--#--{1,8}--#--{2}", 100, -40.2f, " String ");

         在这行代码中,PatternList一共有5个Pattern,分别是:

         

        {0} 参数0
    
         --#-- 原始类型
    
        {1,8} 参数1 宽度8
    
        --#-- 原始类型 纯字符串
    
        {2} 参数2

         这样设计可以优化掉重复的字符串Parse。

    2.各种类型到字符串转换的算法优化

         这部分代码完全存在于文件Algorithm.hpp中,这里面包含了诸如int、double等转换为字符串的快速算法,实测性能优于sprintf和atoi之类。通过这些基础算法的优化,性能可以得到相当不错的提升。

         

     1        template < typename TCharType >
     2         inline void StringReverse(TCharType* Start, TCharType* End)
     3         {
     4             TCharType Aux;
     5 
     6             while (Start < End)
     7             {
     8                 Aux = *End;
     9                 *End-- = *Start;
    10                 *Start++ = Aux;
    11             }
    12         }
    13 
    14         namespace Detail
    15         {
    16             const char DigitMap[] = 
    17             { 
    18                 '0', '1', '2', '3', '4', '5', '6',
    19                 '7', '8', '9', 'A', 'B', 'C', 'D',
    20                 'E', 'F'
    21             };
    22         }       
    23 
    24         template < typename TCharType >
    25         inline SIZE_T Int64ToString(INT64 Value, TCharType* Buf, INT Base)
    26         {
    27             assert(Base > 0 && static_cast<SIZE_T>(Base) <= _countof(Detail::DigitMap));
    28 
    29             TCharType* Str = Buf;
    30 
    31             UINT64 UValue = (Value < 0) ? -Value : Value;
    32 
    33             // Conversion. Number is reversed.
    34             do
    35             {
    36                 *Str++ = Detail::DigitMap[UValue%Base];
    37             } while (UValue /= Base);
    38 
    39             if (Value < 0)
    40             {
    41                 *Str++ = '-';
    42             }
    43 
    44             *Str = '\0';
    45 
    46             // Reverse string
    47             StringReverse<TCharType>(Buf, Str - 1);
    48 
    49             return Str - Buf;
    50         }

           上面这段代码展示的是快速整数到字符串的转换。据说基于sse指令的各种转换会更快,然而担心兼容性问题影响跨平台,我并未采用。

    3. 栈容器和栈字符串

           这部分代码存在于文件Utility.hpp中,这部分代码的优化原理就是在需要的动态内存并不大的时候,直接使用栈内存,当内存需求大到超过一定阀值的时候,自动申请堆内存并将栈数据转移到堆内存上。在大多数情况下,我们需要的内存都是很少,因此在绝大多数情况下,都能起到相当显著的优化效果。

      1         template <
      2             typename T,
      3             INT DefaultLength = 0xFF,
      4             INT ExtraLength = 0
      5         >
      6         class TAutoArray
      7         {
      8         public:
      9             typedef TAutoArray<T, DefaultLength, ExtraLength>   SelfType;
     10 
     11             enum
     12             {
     13                 DEFAULT_LENGTH = DefaultLength
     14             };
     15 
     16             class ConstIterator : Noncopyable
     17             {
     18             public:
     19                 ConstIterator(const SelfType& InRef) :
     20                     Ref(InRef),
     21                     Index( InRef.GetLength()>0?0:-1 )
     22                 {
     23                 }
     24 
     25                 bool IsValid() const
     26                 {
     27                     return Index < Ref.GetLength();
     28                 }
     29 
     30                 bool Next()
     31                 {
     32                     ++Index;
     33 
     34                     return IsValid();
     35                 }
     36 
     37                 const T& operator *() const
     38                 {
     39                     const T* Ptr = Ref.GetDataPtr();
     40 
     41                     return Ptr[Index];
     42                 }
     43             private:
     44                 ConstIterator& operator = (const ConstIterator&);
     45                 ConstIterator(ConstIterator&);
     46             protected:
     47                 const SelfType&   Ref;
     48                 SIZE_T            Index;
     49             };
     50 
     51             TAutoArray() :
     52                 Count(0),
     53                 AllocatedCount(0),
     54                 HeapValPtr(NULL)
     55             {
     56             }
     57 
     58             ~TAutoArray()
     59             {
     60                 ReleaseHeapData();
     61 
     62                 Count = 0;
     63             }
     64 
     65             TAutoArray(const SelfType& Other) :
     66                 Count(Other.Count),
     67                 AllocatedCount(Other.AllocatedCount),
     68                 HeapValPtr(NULL)
     69             {
     70                 if (Count > 0)
     71                 {
     72                     if (Other.IsDataOnStack())
     73                     {
     74                         Algorithm::CopyArray(Other.StackVal, Other.StackVal + Count, StackVal);
     75                     }
     76                     else
     77                     {
     78                         HeapValPtr = Allocate(AllocatedCount);
     79                         Algorithm::CopyArray(Other.HeapValPtr, Other.HeapValPtr + Count, HeapValPtr);
     80                     }
     81                 }
     82             }
     83 
     84             SelfType& operator = (const SelfType& Other)
     85             {
     86                 if (this == &Other)
     87                 {
     88                     return *this;
     89                 }
     90 
     91                 ReleaseHeapData();
     92 
     93                 Count = Other.Count;
     94                 AllocatedCount = Other.AllocatedCount;
     95                 HeapValPtr = NULL;
     96 
     97                 if (Count > 0)
     98                 {
     99                     if (Other.IsDataOnStack())
    100                     {
    101                         Algorithm::CopyArray(Other.StackVal, Other.StackVal + Count, StackVal);
    102                     }
    103                     else
    104                     {
    105                         HeapValPtr = Allocate(AllocatedCount);
    106                         Algorithm::CopyArray(Other.HeapValPtr, Other.HeapValPtr + Count, HeapValPtr);
    107                     }
    108                 }
    109 
    110                 return *this;
    111             }
    112 
    113             SelfType& TakeFrom(SelfType& Other)
    114             {
    115                 if (this == &Other)
    116                 {
    117                     return *this;
    118                 }
    119 
    120                 Count = Other.Count;
    121                 AllocatedCount = Other.AllocatedCount;
    122                 HeapValPtr = Other.HeapValPtr;
    123 
    124                 if (Count > 0 && Other.IsDataOnStack())
    125                 {
    126                     Algorithm::MoveArray(Other.StackVal, Other.StackVal + Count, StackVal);
    127                 }
    128 
    129                 Other.Count = 0;
    130                 Other.AllocatedCount = 0;
    131                 Other.HeapValPtr = NULL;
    132             }
    133 
    134             void TakeTo(SelfType& Other)
    135             {
    136                 Other.TakeFrom(*this);
    137             }
    138 
    139 #if FL_PLATFORM_HAS_RIGHT_VALUE_REFERENCE
    140             TAutoArray( SelfType && Other ) :
    141                 Count(Other.Count),
    142                 AllocatedCount(Other.AllocatedCount),
    143                 HeapValPtr(Other.HeapValPtr)
    144             {
    145                 if (Count > 0 && Other.IsDataOnStack())
    146                 {
    147                     Algorithm::MoveArray(Other.StackVal, Other.StackVal + Count, StackVal);
    148                 }
    149 
    150                 Other.Count = 0;
    151                 Other.AllocatedCount = 0;
    152                 Other.HeapValPtr = NULL;
    153             }
    154 
    155             SelfType& operator = (SelfType&& Other )
    156             {
    157                 return TakeFrom(Other);
    158             }
    159 #endif
    160 
    161             bool  IsDataOnStack() const
    162             {
    163                 return HeapValPtr == NULL;
    164             }
    165 
    166             void  AddItem(const T& InValue)
    167             {
    168                 if (IsDataOnStack())
    169                 {
    170                     if (Count < DEFAULT_LENGTH)
    171                     {
    172                         StackVal[Count] = InValue;
    173                         ++Count;
    174                     }
    175                     else if (Count == DEFAULT_LENGTH)
    176                     {
    177                         InitialMoveDataToHeap();
    178 
    179                         assert(Count < AllocatedCount);
    180 
    181                         HeapValPtr[Count] = InValue;
    182                         ++Count;
    183                     }
    184                     else
    185                     {
    186                         assert(false && "internal error");
    187                     }
    188                 }
    189                 else
    190                 {
    191                     if (Count < AllocatedCount)
    192                     {
    193                         HeapValPtr[Count] = InValue;
    194                         ++Count;
    195                     }
    196                     else
    197                     {
    198                         ExpandHeapSpace();
    199 
    200                         assert(Count < AllocatedCount);
    201                         HeapValPtr[Count] = InValue;
    202                         ++Count;
    203                     }
    204                 }
    205             }
    206 
    207             SIZE_T GetLength() const
    208             {
    209                 return Count;
    210             }
    211 
    212             SIZE_T GetAllocatedCount() const
    213             {
    214                 return AllocatedCount;
    215             }
    216 
    217             T* GetDataPtr()
    218             {
    219                 return IsDataOnStack() ? StackVal : HeapValPtr;
    220             }
    221 
    222             const T* GetDataPtr() const
    223             {
    224                 return IsDataOnStack() ? StackVal : HeapValPtr;
    225             }
    226 
    227             T* GetUnusedPtr()
    228             {
    229                 return IsDataOnStack() ? StackVal + Count : HeapValPtr + Count;
    230             }
    231 
    232             const T* GetUnusedPtr() const
    233             {
    234                 return IsDataOnStack() ? StackVal + Count : HeapValPtr + Count;
    235             }
    236 
    237             SIZE_T GetCapacity() const
    238             {                
    239                 return IsDataOnStack() ?
    240                     DEFAULT_LENGTH - Count :
    241                     AllocatedCount - Count;
    242             }
    243             
    244             T& operator []( SIZE_T Index )
    245             {
    246                 assert( Index < GetLength() );
    247                 
    248                 return GetDataPtr()[Index];
    249             }
    250             
    251             const T& operator []( SIZE_T Index ) const
    252             {
    253                 assert( Index < GetLength() );
    254                 
    255                 return GetDataPtr()[Index];
    256             }
    257 
    258         protected:
    259             void  InitialMoveDataToHeap()
    260             {
    261                 assert(HeapValPtr == NULL);
    262 
    263                 AllocatedCount = DEFAULT_LENGTH * 2;
    264 
    265                 HeapValPtr = Allocate(AllocatedCount);
    266 
    267 #if FL_PLATFORM_HAS_RIGHT_VALUE_REFERENCE
    268                 Algorithm::MoveArray(StackVal, StackVal + Count, HeapValPtr);
    269 #else
    270                 Algorithm::CopyArray(StackVal, StackVal + Count, HeapValPtr);
    271 #endif
    272             }
    273 
    274             void  ExpandHeapSpace()
    275             {
    276                 SIZE_T NewCount = AllocatedCount * 2;
    277                 assert(NewCount > AllocatedCount);
    278 
    279                 T* DataPtr = Allocate(NewCount);
    280 
    281                 assert(DataPtr);
    282 
    283 #if FL_PLATFORM_HAS_RIGHT_VALUE_REFERENCE
    284                 Algorithm::MoveArray(HeapValPtr, HeapValPtr + Count, DataPtr);
    285 #else
    286                 Algorithm::CopyArray(HeapValPtr, HeapValPtr + Count, DataPtr);
    287 #endif
    288                 ReleaseHeapData();
    289 
    290                 HeapValPtr = DataPtr;
    291                 AllocatedCount = NewCount;
    292             }
    293 
    294             void  ReleaseHeapData()
    295             {
    296                 if (HeapValPtr)
    297                 {
    298                     delete[] HeapValPtr;
    299                     HeapValPtr = NULL;
    300                 }
    301 
    302                 AllocatedCount = 0;
    303             }
    304 
    305             static T*  Allocate(const SIZE_T InAllocatedCount)
    306             {
    307                 // +ExtraLength this is a hack method for saving string on it.
    308                 return new T[InAllocatedCount + ExtraLength];
    309             }
    310 
    311         protected:
    312             SIZE_T        Count;
    313             SIZE_T        AllocatedCount;
    314 
    315             // +ExtraLength this is a hack method for saving string on it.
    316             T             StackVal[DEFAULT_LENGTH + ExtraLength];
    317 
    318             T*            HeapValPtr;
    319         };

         上面这段代码展示的就是这个设想的实现。这是一个模板类,基于这个类实现了栈字符串。同时默认的PatternList也是使用这个模板来保存的,这样在节约了大量的内存分配操作之后,性能得到进一步的提升。

      1 // String Wrapper
      2         template < typename TCharType >
      3         class TAutoString :
      4             public TAutoArray< TCharType, 0xFF, 2 >
      5         {
      6         public:
      7             typedef TAutoArray< TCharType, 0xFF, 2 > Super;
      8             typedef Mpl::TCharTraits<TCharType>      CharTraits;
      9             typedef TCharType                        CharType;
     10 
     11 #if !FL_COMPILER_MSVC
     12             using Super::Count;
     13             using Super::AllocatedCount;
     14             using Super::HeapValPtr;
     15             using Super::StackVal;
     16             using Super::Allocate;
     17             using Super::IsDataOnStack;
     18             using Super::DEFAULT_LENGTH;
     19             using Super::GetDataPtr;
     20             using Super::ReleaseHeapData;
     21 #endif
     22 
     23             TAutoString()
     24             {
     25             }
     26 
     27             TAutoString(const CharType* pszStr)
     28             {
     29                 if (pszStr)
     30                 {
     31                     const SIZE_T Length = CharTraits::length(pszStr);
     32 
     33                     Count = Length;
     34 
     35                     if (Length <= DEFAULT_LENGTH)
     36                     {
     37                         CharTraits::copy(pszStr, pszStr + Length, StackVal);
     38                         StackVal[Count] = 0;
     39                     }
     40                     else
     41                     {
     42                         HeapValPtr = Allocate(Length);
     43                         CharTraits::copy(pszStr, pszStr + Length, HeapValPtr);
     44                         HeapValPtr[Count] = 0;
     45                     }
     46                 }
     47             }
     48 
     49             void  AddChar(CharType InValue)
     50             {
     51                 AddItem(InValue);
     52 
     53                 if (IsDataOnStack())
     54                 {
     55                     StackVal[Count] = 0;
     56                 }
     57                 else
     58                 {
     59                     HeapValPtr[Count] = 0;
     60                 }
     61             }
     62 
     63             void AddStr(const CharType* pszStart, const CharType* pszEnd = NULL)
     64             {
     65                 const SIZE_T Length = pszEnd ? pszEnd - pszStart : CharTraits::length(pszStart);
     66 
     67                 if (IsDataOnStack())
     68                 {
     69                     if (Count + Length <= DEFAULT_LENGTH)
     70                     {
     71                         CharTraits::copy(StackVal+Count, pszStart, Length);
     72                         Count += Length;
     73 
     74                         StackVal[Count] = 0;
     75                     }
     76                     else
     77                     {
     78                         assert(!HeapValPtr);
     79 
     80                         AllocatedCount = static_cast<SIZE_T>((Count + Length)*1.5f);
     81                         HeapValPtr = Allocate(AllocatedCount);
     82                         assert(HeapValPtr);
     83 
     84                         if (Count > 0)
     85                         {
     86                             CharTraits::copy(HeapValPtr, StackVal, Count);
     87                         }
     88 
     89                         CharTraits::copy(HeapValPtr+Count, pszStart, Length);
     90 
     91                         Count += Length;
     92 
     93                         HeapValPtr[Count] = 0;
     94                     }
     95                 }
     96                 else
     97                 {
     98                     if (Count + Length <= AllocatedCount)
     99                     {
    100                         CharTraits::copy(HeapValPtr+Count, pszStart, Length);
    101                         Count += Length;
    102 
    103                         HeapValPtr[Count] = 0;
    104                     }
    105                     else
    106                     {
    107                         SIZE_T NewCount = static_cast<SIZE_T>((Count + Length)*1.5f);
    108 
    109                         CharType* DataPtr = Allocate(NewCount);
    110 
    111                         if (Count > 0)
    112                         {
    113                             CharTraits::copy(DataPtr, HeapValPtr, Count);
    114                         }
    115 
    116                         ReleaseHeapData();
    117 
    118                         CharTraits::copy(DataPtr, pszStart, Length);
    119 
    120                         Count += Length;
    121 
    122                         AllocatedCount = NewCount;
    123                         HeapValPtr = DataPtr;
    124 
    125                         HeapValPtr[Count] = 0;
    126                     }
    127                 }
    128             }
    129 
    130             const TCharType* CStr() const
    131             {
    132                 return GetDataPtr();
    133             }
    134 
    135             // is is a internal function
    136             // 
    137             void InjectAdd(SIZE_T InCount)
    138             {
    139                 Count += InCount;
    140 
    141                 assert(IsDataOnStack() ? (Count <= DEFAULT_LENGTH) : (Count < AllocatedCount));
    142             }
    143 
    144         protected:
    145             void  AddItem(const TCharType& InValue)
    146             {
    147                 Super::AddItem(InValue);
    148             }
    149         };

          上面展示的是基于栈内存容器实现的栈字符串,在大多数情况下,我们格式化字符串时都采用栈字符串来保存结果,这样可以显著的提升性能。

          同时栈容器和栈字符串,都特别适合于当临时容器和临时字符串,因为多数情况下它们都优化掉了可能需要动态内存分配的操作。所以它们的使用并不局限于这一个小地方。

    4. 基于C++ 11的优化

           除了引入了C++ 11的容器unordered_map之外,还引入了右值引用等新内容,在某些情况下,可以带来一定的性能提升。

           

     1 #if FL_PLATFORM_HAS_RIGHT_VALUE_REFERENCE
     2             TAutoArray( SelfType && Other ) :
     3                 Count(Other.Count),
     4                 AllocatedCount(Other.AllocatedCount),
     5                 HeapValPtr(Other.HeapValPtr)
     6             {
     7                 if (Count > 0 && Other.IsDataOnStack())
     8                 {
     9                     Algorithm::MoveArray(Other.StackVal, Other.StackVal + Count, StackVal);
    10                 }
    11 
    12                 Other.Count = 0;
    13                 Other.AllocatedCount = 0;
    14                 Other.HeapValPtr = NULL;
    15             }
    16 
    17             SelfType& operator = (SelfType&& Other )
    18             {
    19                 return TakeFrom(Other);
    20             }
    21 #endif

           上面展示的是基于右值引用的优化。

           除此之外还是用了线程局部存储(TLS),这依赖于编译器是否支持。前面提到了我们采用Hash容器来存储Pattern缓存,然而在单线程的时候自然无需多余考虑,当需要支持多线程时,则全局唯一的Hash容器的访问都需要加锁,而加锁是有性能开销的。幸好C++ 11带来了内置的TLS支持,其结果就是每个线程会独立保存一份这样的Pattern缓存,因此无需对其访问加锁,这样无疑效率会更高。缺陷则是会损失部分内存。所有的这些都可以通过预先的宏定义来进行开关,使用者可以自行决定使用TLS还是Lock,或者不支持多线程。

     1         template < typename TPolicy >
     2         class TGlobalPatternStorage : 
     3             public TPatternStorage<TPolicy>
     4         {
     5         public:
     6             static TGlobalPatternStorage* GetStorage()
     7             {
     8 #if FL_WITH_THREAD_LOCAL
     9                 struct ManagedStorage
    10                 {
    11                     typedef Utility::TScopedLocker<System::CriticalSection>  LockerType;
    12                     
    13                     System::CriticalSection                                  ManagedCS;
    14                     Utility::TAutoArray<TGlobalPatternStorage*>              Storages;
    15                     
    16                     ~ManagedStorage()
    17                     {
    18                         LockerType Locker(ManagedCS);
    19                         
    20                         for( SIZE_T i=0; i<Storages.GetLength(); ++i )
    21                         {
    22                             delete Storages[i];
    23                         }
    24                     }
    25                     
    26                     void AddStorage( TGlobalPatternStorage* Storage )
    27                     {
    28                         assert(Storage);
    29                         
    30                         LockerType Locker(ManagedCS);
    31                         
    32                         Storages.AddItem(Storage);
    33                     }
    34                 };
    35                 
    36                 static ManagedStorage StaticManager;
    37                 
    38                 static FL_THREAD_LOCAL TGlobalPatternStorage* StaticStorage = NULL;
    39                 
    40                 if( !StaticStorage )
    41                 {
    42                     StaticStorage = new TGlobalPatternStorage();
    43                     
    44                     StaticManager.AddStorage(StaticStorage);
    45                 }
    46                 
    47                 return StaticStorage;
    48 #else
    49                 static TGlobalPatternStorage StaticStorage;
    50                 return &StaticStorage;
    51 #endif
    52             }
    53         };

           如上所示为项目中使用TLS的代码。

    总结

           在将这一系列的优化结合起来之后,可以使得FL的整体效率处于较高水平,不低于C库函数,同时还具备其它格式化库不具备的功能,对于代码安全性等各方面的增强,都有帮助。下面是Test.cpp的测试结果,FL代表的是使用FL库的耗时,CL代表的C库的耗时,同时此测试模拟了多线程环境。

    Windows Visual Studio 2013 Release下的输出:

    复制代码
    0x64
    Test20, -10.0050,  X ,  X
    0x64
    Test20, -10.0050,  X ,  X
    1920 FLElapse:0.0762746
    1920 CLElapse:0.269722
    1636 FLElapse:0.0756153
    7732 FLElapse:0.0766446
    7956 FLElapse:0.0762051
    7956 CLElapse:0.285714
    1636 CLElapse:0.288648
    7732 CLElapse:0.289193
    复制代码

    Mac Xcode Release:

    复制代码
    99
    Test20, -10.0050,  X ,  X 
    18446744073709551615 FLElapse:0.0901681
    18446744073709551615 CLElapse:0.19329
    18446744073709551615 FLElapse:0.147378
    18446744073709551615 FLElapse:0.150375
    18446744073709551615 FLElapse:0.153342
    18446744073709551615 CLElapse:0.303508
    18446744073709551615 CLElapse:0.308418
    18446744073709551615 CLElapse:0.307407
    复制代码

           

            这并非完全的测试,更多的测试需要在实际使用过程中来验证。

  • 相关阅读:
    编程心得
    关于百分比的小花招
    vue2.0实现银行卡类型种类的选择
    如何运行vue项目(维护他人的项目)
    手把手教你用vue-cli构建一个简单的路由应用
    解决eclipse端口被占用的问题
    echarts统计图踩坑合集
    echarts如何设置背景图的颜色
    小程序获取的用户头像怎么做成圆形
    vue踩坑记-在项目中安装依赖模块npm install报错
  • 原文地址:https://www.cnblogs.com/bodong/p/4800340.html
Copyright © 2020-2023  润新知