第17回コベチケの会

コベリンの最近の取り組みとして、業務などで役立ちそうな知見を共有する会を開催することになりました。 そのついでに発表に使ったアジェンダもそのまま公開してしまおうという豪快な企画です。

※ アジェンダをそのままコピペして公開したものなので若干見にくい箇所もあるかもしれませんが、ご了承ください。

ArraySet @mironal

普段配列っぽいものは Array を使うのが普通だけど、要素が Uniq でかつ順番を気にしなくていいのであれば Set のほうが速度面で有利であることは知られていると思う。

じゃあ一体どのぐらい早いんだ?というのを詳しくみていく.

検証用のコードは以下に示されるものを使用する. 端的に言うと ArraySetcontains の速度を比較するものである。

要素の数は10万個の Int の配列である。 コレを使って以下の3パターンを検証する.

  • Array の contains
  • Array を Set に変換してから contains
  • Set の contains

また、 contains に渡す値は Random な値を使用しており、以下の2パターンある。1つ目は0〜9 までの値なので Array の場合でも比較的速度が劣化しないのではないか?という予想の検証のため。 2つ目は 1000000 までのレンジなので Array の場合は配列の後ろの方まで走査されるため著しく速度が劣化するのではないかと予想。

  • 0..<10
  • 0..<1000000

よって検証は 3*2で6パターンの結果が出る。

これらを100回ずつ loop させてかかった時間の平均値を取る.

import Foundation

func findArray(_ int: Int, in array: [Int]) -> Bool {
  return array.contains(int)
}

func findSet(_ int: Int, in set: Set<Int>) -> Bool {
  return set.contains(int)
}

// 10万個の要素
let bigArray = Array(0..<100000)
let bigSet = Set(bigArray)

let numOfRepeat = 100
var diffSum: TimeInterval = 0

[10, 1_000_000].forEach { maxRange in
  let randomRange: Range<Int> = 0..<maxRange

  // findArray

  for _ in 0..<numOfRepeat {
    let start = Date()
    _ = findArray(Int.random(in: randomRange), in: bigArray)
    let end = Date()
    diffSum += end.timeIntervalSince1970 - start.timeIntervalSince1970
  }
  print("findArray:\(randomRange):", diffSum / TimeInterval(numOfRepeat) * 1000, "[ms]")

  // findSet: 毎回 Set に変換するコストあり

  diffSum = 0

  for _ in 0..<numOfRepeat {
    let start = Date()
    _ = findSet(Int.random(in: randomRange), in: Set(bigArray))
    let end = Date()
    diffSum += end.timeIntervalSince1970 - start.timeIntervalSince1970
  }
  print("findSet with Convert:\(randomRange):", diffSum / TimeInterval(numOfRepeat) * 1000, "[ms]")

  // findSet2 // 毎回 Set に変換しない

  diffSum = 0

  for _ in 0..<numOfRepeat {
    let start = Date()
    _ = findSet(Int.random(in: randomRange), in: bigSet)
    let end = Date()
    diffSum += end.timeIntervalSince1970 - start.timeIntervalSince1970
  }
  print("findSet:\(randomRange):", diffSum / TimeInterval(numOfRepeat) * 1000, "[ms]")
}

https://swiftfiddle.com/6r2ukmt5a5gexnwnp4qpghrbce

2回動かした結果はコレ

// 1回目
findArray:0..<10: 0.003101825714111328 [ms]
findSet with Convert:0..<10: 179.81974124908447 [ms]
findSet:0..<10: 0.00179290771484375 [ms]
findArray:0..<1000000: 45.39460897445679 [ms]
findSet with Convert:0..<1000000: 183.73700857162476 [ms]
findSet:0..<1000000: 0.0019288063049316406 [ms]

// 2回目

findArray:0..<10: 0.0030922889709472656 [ms]
findSet with Convert:0..<10: 174.08085346221924 [ms]
findSet:0..<10: 0.002269744873046875 [ms]
findArray:0..<1000000: 43.42754602432251 [ms]
findSet with Convert:0..<1000000: 173.14964771270752 [ms]
findSet:0..<1000000: 0.00194549560546875 [ms]

整理

Array の contains

0..<10

  • 0.003101825714111328 [ms]
  • 0.0030922889709472656 [ms]

まぁ早い.

0..<1000000

  • 45.39460897445679 [ms]
  • 43.42754602432251 [ms]

遅.... 後ろの方まで走査するのはやっぱりおそそう.

Array を Set に変換してから contains

0..<10

  • 179.81974124908447 [ms]
  • 174.08085346221924 [ms]

遅い. Array -> Set への変換コストが馬鹿にならない模様.

0..<1000000

  • 183.73700857162476 [ms]
  • 173.14964771270752 [ms]

Range が広くなっても速度の劣化はない. Array -> Set への変換コストが支配的な模様.

Set の contains

0..<10

  • 0.00179290771484375 [ms]
  • 0.002269744873046875 [ms]

まぁ早い.

0..<1000000

  • 0.0019288063049316406 [ms]
  • 0.00194549560546875 [ms]

全然早い。

まとめ

  • Set 早い
  • Array は速度の劣化が顕著
  • Array -> Set の変換コストは馬鹿にならないから注意
    • Uniq にするためだけに Array(Set(hogeArray)) みたいなコード書き勝ちかもしれないので注意

UnityのURPでピンクになったお話 @takkumattsu

そもそもURPって?

バージョン2018くらいにSRP Scriptable Render Pipelineという仕組みが登場した。これはリンク先に書いてあるようにUnityでは固定で用意されていたビルドインレンダリングパイプラインに代わるもので、必要に応じてスクリプトで変更できるもの。

docs.unity3d.com

レンダリングパイプラインは、オブジェクトを画面に描画するまでの様々な工程群(カリング、座標変換、陰影計算、ラスタライズなど)を表す言葉らしい

んでSRPはいきなり自分で書くのは難しいので、Unityがあらかじめ用意してくれたのが「URP」と「HDRP

ざっくりURPはモバイルでも使えるやつで、HDRPがハイエンドプラットフォーム向け

プロジェクト作成時に選べる

image.png (124.9 kB)

Unityのバージョンで選ぶ画面は変わってくると思うけど、Unity2020.3.32f1だと「3D」「3D Sample Sence(HDRP)」「3D Sample Sence(URP)」と3Dのテンプレートがある

ピンク地獄

先日作ったノウギョリン 〜我ら収穫の時〜 でぶち当たった問題でUnityがピンクになる地獄に陥った

image.png (1.3 MB)

原因究明

今回利用したAssetはFarm CropsというAsset

assetstore.unity.com

ひとまずピンクについてググったところ

【Unity】AssetBundleに格納したSceneやPrefabがピンクになる問題 - テラシュールブログ tsubakit1.hateblo.jp

どうやらシェーダーが見つからない場合に起きることが多いことが分かった

そしてさらに調べていたところ以下の動画を見つけた

ここで言われていたのは「URPのAssetはURPに対応したプロジェクトでないと動かないよ」とのことだった。 確かに今回作ったのはプロジェクト作成時に普通の3Dのプロジェクトで始めていた。 試しに新しく「3D Sample Sence(URP)」で作ってAssetを入れてみたところ

image.png (563.0 kB)

写ったー!!

無事に解決出来ました。

その後に起きた問題

意気揚々と作っていたところ、再びピンク地獄に遭遇

image.png (166.5 kB)

使ったのはFood storeroomというAsset assetstore.unity.com

結論から言うと、「3D Sample Sence(URP)」で作ったプロジェクトのため旧来の仕組みで動くシェーダーが組み込まれていると、自分で書き直さないといけないらしい

こんな感じで独自シェーダーが入ってるやつは「3D Sample Sence(URP)」で作った時はそのままでは動かせなかった

image.png (71.3 kB)

ちなみに「3D Sample Sence(URP)」で作ったプロジェクトに独自シェーダーが入っていない時もピンクになる時がある

これ原因は同じで昔のビルドインレンダリングパイプラインが利用される想定なので「3D Sample Sence(URP)」で作った時に対応するSRPがないとなっているから、その時は以下のようにマテリアルを選択して「Convert Selected Built-In Materials to URP」を選ぶと新しいSRPに対応したものになりちゃんと表示されるようになる

image.png (870.1 kB)

まとめ

  • ピンクを解決するために「3D Sample Sence(URP)」で初めて解決した
  • しかしFarm CropsがURPにしか対応していなかったので昔の形式のシェーダーが組み込まれているAssetが使えなかった
  • なのでURPだけしか対応していないAssetを使う時は注意が必要!しかしAsset Storeで見分けることはできない(自分は見つけられなかった)
  • 現状
    • 旧プロジェクトではURPオンリーのAssetは使えない(SRPを頑張って書き直せば使える)
    • 「3D Sample Sence(URP)」でプロジェクトを始めた場合は、昔のAssetがピンクになる可能性がある
  • 個人的な最適解
    • →旧プロジェクトで初めて、URPオンリーなAssetに出会ったら諦めるがいいかなと思った

関心の分離 @ryohey

class ProductList {
  items: Product[]

  getProductById(id: String): Product {
    return this.items.find(item => item.id === id)
  }
}
function filterMinusIdItem(items: Product[]) {
    return items.where(item => item.id >= 0 && item.status === "available" && item.subitem.length > 0)
}

interface IDProvider {
  id: string
}

function filterMinusIdItem(items: IDProvider[]) {
    return items.where(item => item.id >= 0)
}

function filterAvailableItems(items: Product[]) {
    return items.where(item => item.status === "available" && item.subitem.length > 0)
}

filterMinusIdItem(filterAvailableItems(items))

関数型でよくある 関数がたくさんになるからたくさん知ってないと書けなくなるデメリットもある ドキュメントはわかりづらくなる

class HogeMock: IDProvider {
  id: string
}

filterMinusIdItem([HogeMock(), ...])

テストが書きやすくなる

class CartList {
  items: Cart[]

  getCartById(id: String): Cart {
    return this.items.find(item => item.id === id)
  }
}

関数の内側から眺める気持ち Cart や Product を知ってる状態になっていて視野が広い

function getItemById<T>(items: T[], id: String): T {
  ...
}
  • クラスや関数の立場になって考える
  • 視野を狭くする
  • 少し知っている状態は、インターフェース使うもの
  • 一般化したことになる
function getItemById<T>(items: T[], id: String): T {
  return FeatherStatusFinder.findById(items, id)
}
  • 最も知らない状態は、プログラミング言語の機能、標準ライブラリの機能のみをつかうもの
  • 視野を非常に狭くした究極にジェネリクスやプリミティブ型だけを参照するコードがある
  • 一般化した関数は強い

強いと何が良いか

  • 別のプロジェクトでも使える
  • みてすぐ理解できる
func fooUseCase() {
    userRepository.getCurrentUser()
      .flatMap { productManager.getProductOwnedByUser($0) }
}

すごい抽象化されてるコードと、一般化されたコードを分ける

interface FooUsecaseUserProvider {
  function getCurrentUser()
}

func fooUseCase(userProvider: FooUsecaseUserProvider) {
    userProvider.getCurrentUser()
}
  • 強さの大きい関数を集めた、色々なことに使えるコードは別のプロジェクトでも使えるライブラリとなる
  • 関数の気持ちになって、内側から見たような

一般化した関数をグローバルに出してもいいか Swift は衝突しやすいから微妙なところ。名前空間がある言語なら気にしないでいい Swift はモジュールを分ければ名前空間が分かれるのでよい

protocol IDProvider {
    var id: String { get }
}

extension Sequence where Element == IDProvider {
    func findById(id: String) -> {
      self.filter {}
    }
}
products.findById(id: "foo")

protocol を定義しておくと型が違うので衝突しない

func findById(items: [Product], id: String) -> {
}

閉じてるのがいい

enum の話

enum Request {
  case registerUser(User)
  case addItem(Item)
  case updateItem(Item)
  case fetchHelpItems
}

extension Request: URLConvertible {
  var url: URL {
    switch self {
      case .registerUser:
      case .fetchHelpItems:
          
    }
  }
}
func showHelp() {
  let res = await apiClient(Request.fetchHelpItems)
}

class, struct だとこれとこれみたいなのできるけど enum にまとまってるとできない enum はデフォルト引数ができない