[objective-c] なぜあなたは象牙を使うのですか?



Answers

私にとっては、通常はパフォーマンスです。 オブジェクトのivarへのアクセスは、そのような構造体を含むメモリーへのポインターを使用してCの構造体メンバーにアクセスするのと同じくらい速いです。 実際、Objective-Cオブジェクトは基本的に動的に割り当てられたメモリ内にあるC構造体です。 これは通常、コードが得られる速さと同じくらい速く、手で最適化されたアセンブリコードでさえそれより速くなることはありません。

ゲッター/設定でivarにアクセスするには、Objective-Cメソッド呼び出しが必要です。これは "通常の" C関数呼び出しよりもはるかに遅く(少なくとも3〜4回)、通常のC関数呼び出しでさえもすでに数倍遅くなっています構造体メンバにアクセスする。 プロパティの属性に応じて、コンパイラによって生成されたセッター/ゲッターの実装には、 objc_getProperty / objc_setPropertyに対する別のC関数呼び出しが必要objc_setProperty 。これらは、必要に応じてオブジェクトをretain / copy / autorelease 、必要に応じてプロパティを設定します。 これは簡単に非常に高価になることがあり、私は50%遅くなることについて話しているわけではありません。

これを試してみましょう:

CFAbsoluteTime cft;
unsigned const kRuns = 1000 * 1000 * 1000;

cft = CFAbsoluteTimeGetCurrent();
for (unsigned i = 0; i < kRuns; i++) {
    testIVar = i;
}
cft = CFAbsoluteTimeGetCurrent() - cft;
NSLog(@"1: %.1f picoseconds/run", (cft * 10000000000.0) / kRuns);

cft = CFAbsoluteTimeGetCurrent();
for (unsigned i = 0; i < kRuns; i++) {
    [self setTestIVar:i];
}
cft = CFAbsoluteTimeGetCurrent() - cft;
NSLog(@"2: %.1f picoseconds/run", (cft * 10000000000.0) / kRuns);

出力:

1: 23.0 picoseconds/run
2: 98.4 picoseconds/run

これは4.28倍遅く、これは非原子的なプリミティブintであり、ほとんど最高のケースです。 他のほとんどのケースはさらに悪いです( NSString *アトミックプロパティを試してみてください)。 だから、各ivarのアクセスがそれより4〜5倍遅くなっているという事実を生かすことができれば、プロパティを使うことは(少なくともパフォーマンスに関しては)うまくいくが、そのようなパフォーマンスの低下がある状況はたくさんある完全に容認できない。

アップデート2015-10-20

これは現実の世界の問題ではなく、上記のコードは純粋に合成されており、実際のアプリケーションではこれを気付かない人もいます。 さて、実際のサンプルを試してみましょう。

以下のコードは、 Accountオブジェクトを定義しています。 アカウントには、所有者の名前( NSString * )、性別( enum )、年齢( unsigned )、残高( int64_t )を記述するプロパティがあります。 アカウントオブジェクトには、 initメソッドとcompare:メソッドがあります。 compare:メソッドは次のように定義されています:男性の前の女性の注文、アルファベット順の名前、古い前の若い注文、低から高の注文のバランス。

実際にはAccountAAccountB 2つのアカウントクラスが存在します。 それらの実装を見てみると、1つの例外を除いて、それらがほぼ完全に同一であることがわかります: compare:メソッド。 AccountAオブジェクトはメソッド(getter)によって独自のプロパティにアクセスし AccountBオブジェクトはAccountBによって独自のプロパティにアクセスします 。 それは本当に唯一の違いです! 彼らは両方ともgetter(他のオブジェクトがサブクラスでgetterをオーバーライドした場合、安全ではないでしょう)で比較する他のオブジェクトのプロパティにアクセスします。 また、ivarsとして自分のプロパティにアクセスしてもカプセル化が中断されないことに注意してください(ivarsはまだ公開されていません)。

テストセットアップは本当に簡単です:1 Mioランダムアカウントを作成し、それらをアレイに追加し、その配列をソートします。 それでおしまい。 もちろん、 AccountAオブジェクト用とAccountAオブジェクト用の2つの配列があり、両方の配列が同じアカウント(同じデータソース)で埋められています。 配列のソートにどれくらい時間がかかります。

私が昨日したいくつかの試合の結果は次のとおりです。

runTime 1: 4.827070, 5.002070, 5.014527, 5.019014, 5.123039
runTime 2: 3.835088, 3.804666, 3.792654, 3.796857, 3.871076

ご覧のように、 AccountBオブジェクトの配列をソートすることは、 AccountBオブジェクトの配列をソートするよりずっと速くなります。

1.32秒までのランタイム差異が何の違いもないと主張する人は、UIプログラミングをしない方がよいでしょう。 たとえば、大きなテーブルの並べ替え順序を変更したい場合、これらのような時差がユーザーに大きな違いをもたらします(受け入れ可能なUIと遅いUIの違い)。

この場合もサンプルコードはここで実行される唯一の実際の作業ですが、コードは複雑なクロックワークの小さなギアですか? そして、すべてのギアがこのようなプロセス全体を減速させるならば、最終的に全時計のスピードはどういう意味ですか? 特に1つの作業ステップが別の作業ステップの出力に依存する場合、すべての非効率性が合計されます。 ほとんどの非効率性はそれ自身では問題ではありませんが、プロセス全体に問題となるのはその完全な合計です。 このような問題は、プロファイラが重要なホットスポットを発見するためにプロファイラが簡単に表示するものではありませんが、これらの非効率性はどれも単独のホットスポットではありません。 CPU時間はそれらの間にちょうど平均的に広がっていますが、それぞれの時間はほんのわずかです。最適化するのに時間が浪費されているようです。 そしてそれは本当です、それらのうちの1つだけを最適化することは絶対に何も助けず、すべてを最適化することは劇的に助けになるでしょう。

CPU時間の無駄遣いが完全に受け入れられると思うので、たとえあなたがCPU時間を考慮していないとしても、それはすべて無料のためだから、消費電力によるサーバホスティングコストはどうなるでしょうか? 携帯端末のバッテリー・ランタイムはどうですか? 同じモバイルアプリケーションを2回(たとえばモバイルWebブラウザ)作成する場合は、すべてのクラスがゲッターだけで独自のプロパティにアクセスするバージョンと、すべてのクラスがivarsだけでアクセスするバージョンを作成すると、最初のバージョンを使用すると常に確実に排除されます電池は、機能的に同等であり、ユーザにとっては、おそらくは少し速く感じることすらあっても、第2の電池を使用するよりはるかに高速である。

main.mファイルのコードをmain.mます(コードはARCが有効になっていることをmain.mとしており、コンパイル時に最適化を使用して完全な効果を確認してください)。

#import <Foundation/Foundation.h>

typedef NS_ENUM(int, Gender) {
    GenderMale,
    GenderFemale
};


@interface AccountA : NSObject
    @property (nonatomic) unsigned age;
    @property (nonatomic) Gender gender;
    @property (nonatomic) int64_t balance;
    @property (nonatomic,nonnull,copy) NSString * name;

    - (NSComparisonResult)compare:(nonnull AccountA *const)account;

    - (nonnull instancetype)initWithName:(nonnull NSString *const)name
        age:(const unsigned)age gender:(const Gender)gender
        balance:(const int64_t)balance;
@end


@interface AccountB : NSObject
    @property (nonatomic) unsigned age;
    @property (nonatomic) Gender gender;
    @property (nonatomic) int64_t balance;
    @property (nonatomic,nonnull,copy) NSString * name;

    - (NSComparisonResult)compare:(nonnull AccountB *const)account;

    - (nonnull instancetype)initWithName:(nonnull NSString *const)name
        age:(const unsigned)age gender:(const Gender)gender
        balance:(const int64_t)balance;
@end


static
NSMutableArray * allAcocuntsA;

static
NSMutableArray * allAccountsB;

static
int64_t getRandom ( const uint64_t min, const uint64_t max ) {
    assert(min <= max);
    uint64_t rnd = arc4random(); // arc4random() returns a 32 bit value only
    rnd = (rnd << 32) | arc4random();
    rnd = rnd % ((max + 1) - min); // Trim it to range
    return (rnd + min); // Lift it up to min value
}

static
void createAccounts ( const NSUInteger ammount ) {
    NSArray *const maleNames = @[
        @"Noah", @"Liam", @"Mason", @"Jacob", @"William",
        @"Ethan", @"Michael", @"Alexander", @"James", @"Daniel"
    ];
    NSArray *const femaleNames = @[
        @"Emma", @"Olivia", @"Sophia", @"Isabella", @"Ava",
        @"Mia", @"Emily", @"Abigail", @"Madison", @"Charlotte"
    ];
    const NSUInteger nameCount = maleNames.count;
    assert(maleNames.count == femaleNames.count); // Better be safe than sorry

    allAcocuntsA = [NSMutableArray arrayWithCapacity:ammount];
    allAccountsB = [NSMutableArray arrayWithCapacity:ammount];

    for (uint64_t i = 0; i < ammount; i++) {
        const Gender g = (getRandom(0, 1) == 0 ? GenderMale : GenderFemale);
        const unsigned age = (unsigned)getRandom(18, 120);
        const int64_t balance = (int64_t)getRandom(0, 200000000) - 100000000;

        NSArray *const nameArray = (g == GenderMale ? maleNames : femaleNames);
        const NSUInteger nameIndex = (NSUInteger)getRandom(0, nameCount - 1);
        NSString *const name = nameArray[nameIndex];

        AccountA *const accountA = [[AccountA alloc]
            initWithName:name age:age gender:g balance:balance
        ];
        AccountB *const accountB = [[AccountB alloc]
            initWithName:name age:age gender:g balance:balance
        ];

        [allAcocuntsA addObject:accountA];
        [allAccountsB addObject:accountB];
    }
}


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        @autoreleasepool {
            NSUInteger ammount = 1000000; // 1 Million;
            if (argc > 1) {
                unsigned long long temp = 0;
                if (1 == sscanf(argv[1], "%llu", &temp)) {
                    // NSUIntegerMax may just be UINT32_MAX!
                    ammount = (NSUInteger)MIN(temp, NSUIntegerMax);
                }
            }
            createAccounts(ammount);
        }

        // Sort A and take time
        const CFAbsoluteTime startTime1 = CFAbsoluteTimeGetCurrent();
        @autoreleasepool {
            [allAcocuntsA sortedArrayUsingSelector:@selector(compare:)];
        }
        const CFAbsoluteTime runTime1 = CFAbsoluteTimeGetCurrent() - startTime1;

        // Sort B and take time
        const CFAbsoluteTime startTime2 = CFAbsoluteTimeGetCurrent();
        @autoreleasepool {
            [allAccountsB sortedArrayUsingSelector:@selector(compare:)];
        }
        const CFAbsoluteTime runTime2 = CFAbsoluteTimeGetCurrent() - startTime2;

        NSLog(@"runTime 1: %f", runTime1);
        NSLog(@"runTime 2: %f", runTime2);
    }
    return 0;
}



@implementation AccountA
    - (NSComparisonResult)compare:(nonnull AccountA *const)account {
        // Sort by gender first! Females prior to males.
        if (self.gender != account.gender) {
            if (self.gender == GenderFemale) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // Otherwise sort by name
        if (![self.name isEqualToString:account.name]) {
            return [self.name compare:account.name];
        }

        // Otherwise sort by age, young to old
        if (self.age != account.age) {
            if (self.age < account.age) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // Last ressort, sort by balance, low to high
        if (self.balance != account.balance) {
            if (self.balance < account.balance) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // If we get here, the are really equal!
        return NSOrderedSame;
    }

    - (nonnull instancetype)initWithName:(nonnull NSString *const)name
        age:(const unsigned)age gender:(const Gender)gender
        balance:(const int64_t)balance
    {
        self = [super init];
        assert(self); // We promissed to never return nil!

        _age = age;
        _gender = gender;
        _balance = balance;
        _name = [name copy];

        return self;
    }
@end


@implementation AccountB
    - (NSComparisonResult)compare:(nonnull AccountA *const)account {
        // Sort by gender first! Females prior to males.
        if (_gender != account.gender) {
            if (_gender == GenderFemale) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // Otherwise sort by name
        if (![_name isEqualToString:account.name]) {
            return [_name compare:account.name];
        }

        // Otherwise sort by age, young to old
        if (_age != account.age) {
            if (_age < account.age) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // Last ressort, sort by balance, low to high
        if (_balance != account.balance) {
            if (_balance < account.balance) return NSOrderedAscending;
            return NSOrderedDescending;
        }

        // If we get here, the are really equal!
        return NSOrderedSame;
    }

    - (nonnull instancetype)initWithName:(nonnull NSString *const)name
        age:(const unsigned)age gender:(const Gender)gender
        balance:(const int64_t)balance
    {
        self = [super init];
        assert(self); // We promissed to never return nil!

        _age = age;
        _gender = gender;
        _balance = balance;
        _name = [name copy];

        return self;
    }
@end
Question

私は通常、このような質問は、 すべてのivarはプロパティでなければならないなど、他の方法を求めて参照してください (そして、私はこのQへのbbumの答えが好きです)。

ほとんど私のコードでプロパティを使用しています。 しかし、しばしば、私は長い間iOS上で開発してきた伝統的なゲームプログラマーである請負業者と協力しています。 彼は、ほとんどすべての特性を宣言し、イヴァールに傾倒するコードを書いています。 Objective C 2.0(Oct '07)とゲッター/セッターを通らないというパフォーマンスの最小限の利益のためにプロパティが常に存在するとは限らないので、彼はそれに慣れています。

彼は漏れのないコードを書いているが、私はまだイヴァールよりもプロパティを使うことを好むだろう。 私たちはそれについて話をしました。彼はKVOを使用していなかったのでプロパティを使用する理由を多かれ少なかれ見ており、彼は記憶上の問題を処理する経験があります。

私の質問はもっと...なぜあなたは経験豊かであろうとなかろうと、ivarの期間を使いたいのですか? パフォーマンスの違いは本当に素晴らしいですか?ivarを使用すると正当化されるでしょうか?

また、明確化のポイントとして、必要に応じてセッターとゲッターをオーバーライドし、ゲッタ/セッター内部のそのプロパティと相関するivarを使用します。 しかし、getter / setterやinitの外部では、私は常にself.myProperty構文を使用します。

1を編集

私はすべての良い反応に感謝します。 私が対処したいのは、間違っていると思われるのは、あなたが持っていないプロパティでカプセル化されたivarでカプセル化されるということです。 クラス継承でプロパティを定義するだけです。 これは外部からの財産を隠すでしょう。 また、インターフェイスでプロパティを読み取り専用に宣言し、次のような実装でreadwriteとして再定義することもできます。

// readonly for outsiders
@property (nonatomic, copy, readonly) NSString * name;

授業中に継続しています:

// readwrite within this file
@property (nonatomic, copy) NSString * name;

それを完全に "プライベート"にするには、クラスの継続で宣言するだけです。




プロパティとインスタンス変数はトレードオフの関係にあり、最終的にはその選択肢がアプリケーションに伝わります。

カプセル化/情報隠蔽これはデザインの観点からはGood Thing(TM)であり、狭いインターフェースと最小限のリンケージはソフトウェアの保守性と理解可能性を高めます。 Obj-Cでは何も隠すのがかなり難しいですが、 実装で宣言されたインスタンス変数は近いうちに得られます。

パフォーマンス "時期尚早の最適化"はBad Thing(TM)ですが、パフォーマンスが悪いコードを書くのは、最低でも悪いことではないからです。 メソッド呼び出しが負荷やストアよりも高価であると主張するのは難しく、計算集中的なコードではすぐにコストが増加します。

C#のようなプロパティを持つ静的言語では、setter / getterへの呼び出しを、コンパイラによって最適化することができます。 しかし、Obj-Cは動的であり、そのような呼び出しを削除することははるかに困難です。

抽象化 Obj-Cのインスタンス変数に対する引数は、伝統的にメモリ管理でした。 MRCインスタンス変数を使用すると、コード全体に広がるように保持/解放/自動解放を呼び出す必要があり、MRCコードを1つの場所、つまりGood Thing(TM)である抽象化の原則に保ちます。 しかし、GCやARCではこの引数がなくなり、メモリ管理の抽象化はもはやインスタンス変数に対する引数はありません。




セマンティクス

  • ivarsはできないことを@propertyが表現できます: nonatomiccopy
  • どのようなivars表現することができます@propertyはできません:
    • @protected :サブクラス上のpublic、外部のprivate。
    • @package :64ビットのフレームワーク上のpublic、private outside。 32ビットで@publicと同じです。 アップルの64ビット・クラスとインスタンス変数アクセス制御を参照してください。
    • 修飾子。 例えば、強いオブジェクト参照の配列: id __strong *_objs

パフォーマンス

短編小説:イーバールの方が速いですが、ほとんどの用途で問題ありません。 nonatomic的なプロパティはロックを使用しませんが、ダイレクトnonatomicはアクセサー呼び出しをスキップするので高速です。 詳細については、lists.apple.comから以下のemailをお読みください。

Subject: Re: when do you use properties vs. ivars?
From: John McCall <email@hidden>
Date: Sun, 17 Mar 2013 15:10:46 -0700

プロパティはさまざまな方法でパフォーマンスに影響します。

  1. すでに説明したように、ロード/ストアを実行するメッセージを送信することは、ロード/ストアをインラインで実行するよりも遅くなります

  2. ロード/ストアを行うためのメッセージを送信することは、i-cacheに保持する必要があるコードのほんの少しです。ゲッター/セッターがロード/ストアだけの余分な命令を追加したとしても、メッセージを設定して結果を送信して処理するために、呼び出し元の追加の命令を追加します。

  3. メッセージを送信すると、そのセレクタのエントリがメソッドキャッシュに保持され、そのメモリは一般にdキャッシュ内に固執します。 これにより、起動時間が長くなり、アプリの静的メモリ使用量が増え、コンテキストスイッチが苦労します。 メソッドキャッシュはオブジェクトの動的クラスに固有のものなので、この問題はKVOを使用するほど増加します。

  4. メッセージを送信すると、関数内のすべての値がスタックに流されます (または、呼び出し先保存レジスタに保持されます。これは別の時間に流出することを意味します)。

  5. メッセージの送信には任意の副作用があり、したがって

    • コンパイラは非ローカルメモリに関するすべての前提条件をリセットします
    • 吊るしたり、沈めたり、並べ替えたり、合体させたり、排除することはできません。

  6. ARCでは、メッセージの送信結果は 、呼び出し元または呼び出し元によって+ 0の戻り値であっても常に保持されます。メソッドがその結果を保持または自動解放しなくても、呼び出し元はそのことを知りません。その結果がオートリリースされるのを防ぐために行動を起こそうとします。 メッセージ送信が静的に解析可能ではないため、これは決して排除できません。

  7. ARCでは、セッターメソッドは一般的に+0で引数を取るため、そのオブジェクトの保持(前述のようにARCが通常持つ)をivarに「転送」する方法がないため、値は一般に取得する必要があります保持/解放する

これらのどれも、常に悪いことを意味するものではありません。プロパティを使用する理由はたくさんあります。 他の多くの言語機能と同様、無料ではないことにご留意ください。


ジョン






Related