11

AppKit で多数の NSString オブジェクト (100 万など) の幅を非常に高速に測定する方法はありますか? 私はこれを行うために3つの異なる方法を試しました:

  • [NSString sizeWithAttributes:]
  • [NSAttributedString サイズ]
  • NSLayoutManager (高さではなくテキストの幅を取得)

    いくつかのパフォーマンス メトリックを次に示します 。

    Count\Mechanism     sizeWithAttributes     NSAttributedString     NSLayoutManager
    1000                0.057                  0.031                  0.007
    10000               0.329                  0.325                  0.064
    100000              3.06                   3.14                   0.689
    1000000             29.5                   31.3                   7.06



    NSLayoutManager は明らかに進むべき道ですが、問題は

  • 重い NSTextStorageオブジェクト が作成されるため、メモリ フットプリントが高くなります(プロファイラによると 1GB 以上) 。
  • 高い作成時間。かかる時間はすべて、上記の文字列の作成中にあり、それ自体が大きな問題です (その後、グリフが作成され配置された NSTextStorage オブジェクトを測定するのに約 0.0002 秒しかかかりません)。
  • 私がやろうとしていることには、7秒はまだ遅すぎます。もっと速い方法はありますか?約 1 秒で 100 万本の弦を測定するには?

    遊んでみたい場合は、こちらが github プロジェクトです。

  • 4

    1 に答える 1

    3

    ここに私が試していないいくつかのアイデアがあります。

    1. Core Textを直接使用します。他の API はその上に構築されます。

    2. 並列化します。最新のすべての Mac (および最新のすべての iOS デバイスでさえも) には、複数のコアがあります。string 配列を複数のサブ配列に分割します。サブアレイごとに、ブロックをグローバル GCD キューに送信します。ブロックで、必要なコア テキストまたはNSLayoutManagerオブジェクトを作成し、サブ配列内の文字列を測定します。このように、両方の API を安全に使用できます。(コアテキスト) ( NSLayoutManager)

    3. 「高いメモリ フットプリント」について:ローカル自動解放プール ブロックを使用して、ピーク メモリ フットプリントを削減します。

    4. 「かかる時間はすべて上記の文字列の作成中にあり、それ自体がディールブレーカーです」: すべての時間がこれらの行に費やされていると言っています:

      double random = (double)arc4random_uniform(1000) / 1000;
      NSString *randomNumber = [NSString stringWithFormat:@"%f", random];
      

      浮動小数点数のフォーマットにはコストがかかります。これはあなたの本当のユースケースですか?0 ≤ n < 1000 に対して n/1000 の形式のランダムな有理数をフォーマットしたいだけなら、もっと速い方法があります。また、多くのフォントでは、すべての桁が同じ幅になっているため、数字の列を簡単にタイプセットできます。このようなフォントを選択すると、そもそも文字列の測定を回避できます。

    アップデート

    Core Text を使用して思いついた最速のコードを次に示します。ディスパッチ バージョンは、私の Core i7 MacBook Pro のシングル スレッド バージョンのほぼ 2 倍の速さです。あなたのプロジェクトの私のフォークはここにあります。

    static CGFloat maxWidthOfStringsUsingCTFramesetter(
            NSArray *strings, NSRange range) {
        NSString *bigString =
            [[strings subarrayWithRange:range] componentsJoinedByString:@"\n"];
        NSAttributedString *richText =
            [[NSAttributedString alloc]
                initWithString:bigString
                attributes:@{ NSFontAttributeName: (__bridge NSFont *)font }];
        CGPathRef path =
            CGPathCreateWithRect(CGRectMake(0, 0, CGFLOAT_MAX, CGFLOAT_MAX), NULL);
        CGFloat width = 0.0;
        CTFramesetterRef setter =
            CTFramesetterCreateWithAttributedString(
                (__bridge CFAttributedStringRef)richText);
        CTFrameRef frame =
            CTFramesetterCreateFrame(
                setter, CFRangeMake(0, bigString.length), path, NULL);
        NSArray *lines = (__bridge NSArray *)CTFrameGetLines(frame);
        for (id item in lines) {
            CTLineRef line = (__bridge CTLineRef)item;
            width = MAX(width, CTLineGetTypographicBounds(line, NULL, NULL, NULL));
        }
        CFRelease(frame);
        CFRelease(setter);
        CFRelease(path);
        return (CGFloat)width;
    }
    
    static void test_CTFramesetter() {
        runTest(__func__, ^{
            return maxWidthOfStringsUsingCTFramesetter(
                testStrings, NSMakeRange(0, testStrings.count));
        });
    }
    
    static void test_CTFramesetter_dispatched() {
        runTest(__func__, ^{
            dispatch_queue_t gatherQueue = dispatch_queue_create(
                "test_CTFramesetter_dispatched result-gathering queue", nil);
            dispatch_queue_t runQueue =
                dispatch_get_global_queue(QOS_CLASS_UTILITY, 0);
            dispatch_group_t group = dispatch_group_create();
    
            __block CGFloat gatheredWidth = 0.0;
    
            const size_t Parallelism = 16;
            const size_t totalCount = testStrings.count;
            // Force unsigned long to get 64-bit math to avoid overflow for
            // large totalCounts.
            for (unsigned long i = 0; i < Parallelism; ++i) {
                NSUInteger start = (totalCount * i) / Parallelism;
                NSUInteger end = (totalCount * (i + 1)) / Parallelism;
                NSRange range = NSMakeRange(start, end - start);
                dispatch_group_async(group, runQueue, ^{
                    double width =
                        maxWidthOfStringsUsingCTFramesetter(testStrings, range);
                    dispatch_sync(gatherQueue, ^{
                        gatheredWidth = MAX(gatheredWidth, width);
                    });
                });
            }
    
            dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
            return gatheredWidth;
        });
    }
    
    于 2015-05-31T20:44:22.867 に答える